forked from mbk-lab/rui_orig
Initialization
This commit is contained in:
parent
bdb490c953
commit
73e7184395
|
@ -0,0 +1,14 @@
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, build with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
.DS_Store
|
||||||
|
demo/__debug_bin
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceRoot}/demo",
|
||||||
|
//"program": "${workspaceRoot}/editor",
|
||||||
|
"env": {},
|
||||||
|
"args": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"cSpell.words": [
|
||||||
|
"anoshenko",
|
||||||
|
"helvetica",
|
||||||
|
"htmlid",
|
||||||
|
"nesw",
|
||||||
|
"nwse",
|
||||||
|
"onclick",
|
||||||
|
"onkeydown",
|
||||||
|
"onmousedown",
|
||||||
|
"upgrader"
|
||||||
|
]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,40 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// AbsoluteLayout - list-container of View
|
||||||
|
type AbsoluteLayout interface {
|
||||||
|
ViewsContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type absoluteLayoutData struct {
|
||||||
|
viewsContainerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAbsoluteLayout create new AbsoluteLayout object and return it
|
||||||
|
func NewAbsoluteLayout(session Session, params Params) AbsoluteLayout {
|
||||||
|
view := new(absoluteLayoutData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAbsoluteLayout(session Session) View {
|
||||||
|
return NewAbsoluteLayout(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of ViewsContainer by default values
|
||||||
|
func (layout *absoluteLayoutData) Init(session Session) {
|
||||||
|
layout.viewsContainerData.Init(session)
|
||||||
|
layout.tag = "AbsoluteLayout"
|
||||||
|
layout.systemClass = "ruiAbsoluteLayout"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *absoluteLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
if layout.views != nil {
|
||||||
|
for _, view := range layout.views {
|
||||||
|
view.addToCSSStyle(map[string]string{`position`: `absolute`})
|
||||||
|
viewHTML(view, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,212 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AngleUnitType : type of enumerated constants for define a type of AngleUnit value.
|
||||||
|
// Can take the following values: Radian, Degree, Gradian, and Turn
|
||||||
|
type AngleUnitType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Radian - angle in radians
|
||||||
|
Radian AngleUnitType = 0
|
||||||
|
// Radian - angle in radians * π
|
||||||
|
PiRadian AngleUnitType = 1
|
||||||
|
// Degree - angle in degrees
|
||||||
|
Degree AngleUnitType = 2
|
||||||
|
// Gradian - angle in gradian (1⁄400 of a full circle)
|
||||||
|
Gradian AngleUnitType = 3
|
||||||
|
// Turn - angle in turns (1 turn = 360 degree)
|
||||||
|
Turn AngleUnitType = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// AngleUnit describe a size (Value field) and size unit (Type field).
|
||||||
|
type AngleUnit struct {
|
||||||
|
Type AngleUnitType
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deg creates AngleUnit with Degree type
|
||||||
|
func Deg(value float64) AngleUnit {
|
||||||
|
return AngleUnit{Type: Degree, Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rad create AngleUnit with Radian type
|
||||||
|
func Rad(value float64) AngleUnit {
|
||||||
|
return AngleUnit{Type: Radian, Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PiRad create AngleUnit with PiRadian type
|
||||||
|
func PiRad(value float64) AngleUnit {
|
||||||
|
return AngleUnit{Type: PiRadian, Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grad create AngleUnit with Gradian type
|
||||||
|
func Grad(value float64) AngleUnit {
|
||||||
|
return AngleUnit{Type: Gradian, Value: value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal compare two AngleUnit. Return true if AngleUnit are equal
|
||||||
|
func (angle AngleUnit) Equal(size2 AngleUnit) bool {
|
||||||
|
return angle.Type == size2.Type && angle.Value == size2.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func angleUnitSuffixes() map[AngleUnitType]string {
|
||||||
|
return map[AngleUnitType]string{
|
||||||
|
Degree: "deg",
|
||||||
|
Radian: "rad",
|
||||||
|
PiRadian: "pi",
|
||||||
|
Gradian: "grad",
|
||||||
|
Turn: "turn",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToAngleUnit converts the string argument to AngleUnit
|
||||||
|
func StringToAngleUnit(value string) (AngleUnit, bool) {
|
||||||
|
var angle AngleUnit
|
||||||
|
ok, err := angle.setValue(value)
|
||||||
|
if !ok {
|
||||||
|
ErrorLog(err)
|
||||||
|
}
|
||||||
|
return angle, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (angle *AngleUnit) setValue(value string) (bool, string) {
|
||||||
|
value = strings.ToLower(strings.Trim(value, " \t\n\r"))
|
||||||
|
|
||||||
|
setValue := func(suffix string, unitType AngleUnitType) (bool, string) {
|
||||||
|
val, err := strconv.ParseFloat(value[:len(value)-len(suffix)], 64)
|
||||||
|
if err != nil {
|
||||||
|
return false, `AngleUnit.SetValue("` + value + `") error: ` + err.Error()
|
||||||
|
}
|
||||||
|
angle.Value = val
|
||||||
|
angle.Type = unitType
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == "π" {
|
||||||
|
angle.Value = 1
|
||||||
|
angle.Type = PiRadian
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(value, "π") {
|
||||||
|
return setValue("π", PiRadian)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(value, "°") {
|
||||||
|
return setValue("°", Degree)
|
||||||
|
}
|
||||||
|
|
||||||
|
for unitType, suffix := range angleUnitSuffixes() {
|
||||||
|
if strings.HasSuffix(value, suffix) {
|
||||||
|
return setValue(suffix, unitType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
angle.Value = val
|
||||||
|
angle.Type = Radian
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, `AngleUnit.SetValue("` + value + `") error: invalid argument`
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - convert AngleUnit to string
|
||||||
|
func (angle AngleUnit) String() string {
|
||||||
|
if suffix, ok := angleUnitSuffixes()[angle.Type]; ok {
|
||||||
|
return fmt.Sprintf("%g%s", angle.Value, suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%g", angle.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cssString - convert AngleUnit to string
|
||||||
|
func (angle AngleUnit) cssString() string {
|
||||||
|
if angle.Type == PiRadian {
|
||||||
|
return fmt.Sprintf("%grad", angle.Value*math.Pi)
|
||||||
|
}
|
||||||
|
|
||||||
|
return angle.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDegree returns the angle in radians
|
||||||
|
func (angle AngleUnit) ToRadian() AngleUnit {
|
||||||
|
switch angle.Type {
|
||||||
|
case PiRadian:
|
||||||
|
return AngleUnit{Value: angle.Value * math.Pi, Type: Radian}
|
||||||
|
|
||||||
|
case Degree:
|
||||||
|
return AngleUnit{Value: angle.Value * math.Pi / 180, Type: Radian}
|
||||||
|
|
||||||
|
case Gradian:
|
||||||
|
return AngleUnit{Value: angle.Value * math.Pi / 200, Type: Radian}
|
||||||
|
|
||||||
|
case Turn:
|
||||||
|
return AngleUnit{Value: angle.Value * 2 * math.Pi, Type: Radian}
|
||||||
|
}
|
||||||
|
|
||||||
|
return angle
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDegree returns the angle in degrees
|
||||||
|
func (angle AngleUnit) ToDegree() AngleUnit {
|
||||||
|
switch angle.Type {
|
||||||
|
case Radian:
|
||||||
|
return AngleUnit{Value: angle.Value * 180 / math.Pi, Type: Degree}
|
||||||
|
|
||||||
|
case PiRadian:
|
||||||
|
return AngleUnit{Value: angle.Value * 180, Type: Degree}
|
||||||
|
|
||||||
|
case Gradian:
|
||||||
|
return AngleUnit{Value: angle.Value * 360 / 400, Type: Degree}
|
||||||
|
|
||||||
|
case Turn:
|
||||||
|
return AngleUnit{Value: angle.Value * 360, Type: Degree}
|
||||||
|
}
|
||||||
|
|
||||||
|
return angle
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToGradian returns the angle in gradians (1⁄400 of a full circle)
|
||||||
|
func (angle AngleUnit) ToGradian() AngleUnit {
|
||||||
|
switch angle.Type {
|
||||||
|
case Radian:
|
||||||
|
return AngleUnit{Value: angle.Value * 200 / math.Pi, Type: Gradian}
|
||||||
|
|
||||||
|
case PiRadian:
|
||||||
|
return AngleUnit{Value: angle.Value * 200, Type: Gradian}
|
||||||
|
|
||||||
|
case Degree:
|
||||||
|
return AngleUnit{Value: angle.Value * 400 / 360, Type: Gradian}
|
||||||
|
|
||||||
|
case Turn:
|
||||||
|
return AngleUnit{Value: angle.Value * 400, Type: Gradian}
|
||||||
|
}
|
||||||
|
|
||||||
|
return angle
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToTurn returns the angle in turns (1 turn = 360 degree)
|
||||||
|
func (angle AngleUnit) ToTurn() AngleUnit {
|
||||||
|
switch angle.Type {
|
||||||
|
case Radian:
|
||||||
|
return AngleUnit{Value: angle.Value / (2 * math.Pi), Type: Turn}
|
||||||
|
|
||||||
|
case PiRadian:
|
||||||
|
return AngleUnit{Value: angle.Value / 2, Type: Turn}
|
||||||
|
|
||||||
|
case Degree:
|
||||||
|
return AngleUnit{Value: angle.Value / 360, Type: Turn}
|
||||||
|
|
||||||
|
case Gradian:
|
||||||
|
return AngleUnit{Value: angle.Value / 400, Type: Turn}
|
||||||
|
}
|
||||||
|
|
||||||
|
return angle
|
||||||
|
}
|
|
@ -0,0 +1,229 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
/*
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnimationTags struct {
|
||||||
|
Tag string
|
||||||
|
Start, End interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnimationKeyFrame struct {
|
||||||
|
KeyFrame int
|
||||||
|
TimingFunction string
|
||||||
|
Params Params
|
||||||
|
}
|
||||||
|
|
||||||
|
type AnimationScenario interface {
|
||||||
|
fmt.Stringer
|
||||||
|
ruiStringer
|
||||||
|
Name() string
|
||||||
|
cssString(session Session) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type animationScenario struct {
|
||||||
|
name string
|
||||||
|
tags []AnimationTags
|
||||||
|
keyFrames []AnimationKeyFrame
|
||||||
|
cssText string
|
||||||
|
}
|
||||||
|
|
||||||
|
var animationScenarios = []string{}
|
||||||
|
|
||||||
|
func addAnimationScenario(name string) string {
|
||||||
|
animationScenarios = append(animationScenarios, name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerAnimationScenario() string {
|
||||||
|
find := func(text string) bool {
|
||||||
|
for _, scenario := range animationScenarios {
|
||||||
|
if scenario == text {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
n := 1
|
||||||
|
name := fmt.Sprintf("scenario%08d", n)
|
||||||
|
for find(name) {
|
||||||
|
n++
|
||||||
|
name = fmt.Sprintf("scenario%08d", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
animationScenarios = append(animationScenarios, name)
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnimationScenario(tags []AnimationTags, keyFrames []AnimationKeyFrame) AnimationScenario {
|
||||||
|
if tags == nil {
|
||||||
|
ErrorLog(`Nil "tags" argument is not allowed.`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tags) == 0 {
|
||||||
|
ErrorLog(`An empty "tags" argument is not allowed.`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
animation := new(animationScenario)
|
||||||
|
animation.tags = tags
|
||||||
|
if keyFrames == nil && len(keyFrames) > 0 {
|
||||||
|
animation.keyFrames = keyFrames
|
||||||
|
}
|
||||||
|
animation.name = registerAnimationScenario()
|
||||||
|
|
||||||
|
return animation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (animation *animationScenario) Name() string {
|
||||||
|
return animation.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (animation *animationScenario) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
animation.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (animation *animationScenario) ruiString(writer ruiWriter) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToCSS(tag string, value interface{}, session Session) string {
|
||||||
|
if value == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
convertFloat := func(val float64) string {
|
||||||
|
if _, ok := sizeProperties[tag]; ok {
|
||||||
|
return fmt.Sprintf("%gpx", val)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%g", val)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
value, ok := session.resolveConstants(value)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if _, ok := sizeProperties[tag]; ok {
|
||||||
|
var size SizeUnit
|
||||||
|
if size.SetValue(value) {
|
||||||
|
return size.cssString("auto")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if isPropertyInList(tag, colorProperties) {
|
||||||
|
var color Color
|
||||||
|
if color.SetValue(value) {
|
||||||
|
return color.cssString()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if isPropertyInList(tag, angleProperties) {
|
||||||
|
var angle AngleUnit
|
||||||
|
if angle.SetValue(value) {
|
||||||
|
return angle.cssString()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if _, ok := enumProperties[tag]; ok {
|
||||||
|
var size SizeUnit
|
||||||
|
if size.SetValue(value) {
|
||||||
|
return size.cssString("auto")
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
return value.cssString("auto")
|
||||||
|
|
||||||
|
case AngleUnit:
|
||||||
|
return value.cssString()
|
||||||
|
|
||||||
|
case Color:
|
||||||
|
return value.cssString()
|
||||||
|
|
||||||
|
case float32:
|
||||||
|
return convertFloat(float64(value))
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
return convertFloat(value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
if prop, ok := enumProperties[tag]; ok {
|
||||||
|
values := prop.cssValues
|
||||||
|
if n >= 0 && n < len(values) {
|
||||||
|
return values[n]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertFloat(float64(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (animation *animationScenario) cssString(session Session) string {
|
||||||
|
if animation.cssText != "" {
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
writeValue := func(tag string, value interface{}) {
|
||||||
|
if cssValue := valueToCSS(tag, value); cssValue != "" {
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
buffer.WriteString(tag)
|
||||||
|
buffer.WriteString(": ")
|
||||||
|
buffer.WriteString(cssValue)
|
||||||
|
buffer.WriteString(";\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`@keyframes `)
|
||||||
|
buffer.WriteString(animation.name)
|
||||||
|
|
||||||
|
buffer.WriteString(" {\n from {\n")
|
||||||
|
for _, property := range animation.tags {
|
||||||
|
writeValue(property.Tag, property.Start)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(" }\n to {\n")
|
||||||
|
for _, property := range animation.tags {
|
||||||
|
writeValue(property.Tag, property.End)
|
||||||
|
}
|
||||||
|
buffer.WriteString(" }\n")
|
||||||
|
|
||||||
|
if animation.keyFrames != nil {
|
||||||
|
for _, keyFrame := range animation.keyFrames {
|
||||||
|
if keyFrame.KeyFrame > 0 && keyFrame.KeyFrame < 100 &&
|
||||||
|
keyFrame.Params != nil && len(keyFrame.Params) > 0 {
|
||||||
|
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
buffer.WriteString(strconv.Itoa(keyFrame.KeyFrame))
|
||||||
|
buffer.WriteString("% {\n")
|
||||||
|
for tag, value := range keyFrame.Params {
|
||||||
|
writeValue(tag, value)
|
||||||
|
}
|
||||||
|
buffer.WriteString(" }\n")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.WriteString("}\n")
|
||||||
|
|
||||||
|
animation.cssText = buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return animation.cssText
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,74 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProtocolInDebugLog If it is set to true, then the protocol of the exchange between
|
||||||
|
// clients and the server is displayed in the debug log
|
||||||
|
var ProtocolInDebugLog = false
|
||||||
|
|
||||||
|
var debugLogFunc func(string) = func(text string) {
|
||||||
|
log.Println("\033[34m" + text)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorLogFunc = func(text string) {
|
||||||
|
log.Println("\033[31m" + text)
|
||||||
|
//println(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDebugLog sets a function for outputting debug info.
|
||||||
|
// The default value is nil (debug info is ignored)
|
||||||
|
func SetDebugLog(f func(string)) {
|
||||||
|
debugLogFunc = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetErrorLog sets a function for outputting error messages.
|
||||||
|
// The default value is log.Println(text)
|
||||||
|
func SetErrorLog(f func(string)) {
|
||||||
|
errorLogFunc = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugLog print the text to the debug log
|
||||||
|
func DebugLog(text string) {
|
||||||
|
if debugLogFunc != nil {
|
||||||
|
debugLogFunc(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugLogF print the text to the debug log
|
||||||
|
func DebugLogF(format string, a ...interface{}) {
|
||||||
|
if debugLogFunc != nil {
|
||||||
|
debugLogFunc(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorLog print the text to the error log
|
||||||
|
func ErrorLog(text string) {
|
||||||
|
if errorLogFunc != nil {
|
||||||
|
errorLogFunc(text)
|
||||||
|
errorStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorLogF print the text to the error log
|
||||||
|
func ErrorLogF(format string, a ...interface{}) {
|
||||||
|
if errorLogFunc != nil {
|
||||||
|
errorLogFunc(fmt.Sprintf(format, a...))
|
||||||
|
errorStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorStack() {
|
||||||
|
if errorLogFunc != nil {
|
||||||
|
skip := 2
|
||||||
|
_, file, line, ok := runtime.Caller(skip)
|
||||||
|
for ok {
|
||||||
|
errorLogFunc(fmt.Sprintf("\t%s: line %d", file, line))
|
||||||
|
skip++
|
||||||
|
_, file, line, ok = runtime.Caller(skip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,124 @@
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 1px;
|
||||||
|
min-height: 1px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
padding: 4px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
padding: 4px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiRoot {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiPopupLayer {
|
||||||
|
background-color: rgba(128,128,128,0.1);
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
right: 0px;
|
||||||
|
left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiView {
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiAbsoluteLayout {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiGridLayout {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiListLayout {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiStackLayout {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiStackPageLayout {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-items: stretch;
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-column-end: 2;
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-row-end: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiTabsLayout {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiImageView {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruiListView {
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
align-content: stretch;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
body {
|
||||||
|
background: #FFF;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.ruiRoot {
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background: #303030;
|
||||||
|
color: #F0F0F0;
|
||||||
|
}
|
||||||
|
.ruiRoot {
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,297 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed app_scripts.js
|
||||||
|
var defaultScripts string
|
||||||
|
|
||||||
|
//go:embed app_styles.css
|
||||||
|
var appStyles string
|
||||||
|
|
||||||
|
//go:embed defaultTheme.rui
|
||||||
|
var defaultThemeText string
|
||||||
|
|
||||||
|
// Application - app interface
|
||||||
|
type Application interface {
|
||||||
|
// Start - start the application life cycle
|
||||||
|
Start(addr string)
|
||||||
|
Finish()
|
||||||
|
nextSessionID() int
|
||||||
|
removeSession(id int)
|
||||||
|
}
|
||||||
|
|
||||||
|
type application struct {
|
||||||
|
name, icon string
|
||||||
|
createContentFunc func(Session) SessionContent
|
||||||
|
sessions map[int]Session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) getStartPage() string {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>`)
|
||||||
|
buffer.WriteString(app.name)
|
||||||
|
buffer.WriteString("</title>")
|
||||||
|
if app.icon != "" {
|
||||||
|
buffer.WriteString(`
|
||||||
|
<link rel="icon" href="`)
|
||||||
|
buffer.WriteString(app.icon)
|
||||||
|
buffer.WriteString(`">`)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`
|
||||||
|
<base target="_blank" rel="noopener">
|
||||||
|
<meta name="viewport" content="width=device-width">
|
||||||
|
<style>`)
|
||||||
|
buffer.WriteString(appStyles)
|
||||||
|
buffer.WriteString(`</style>
|
||||||
|
<script>`)
|
||||||
|
buffer.WriteString(defaultScripts)
|
||||||
|
buffer.WriteString(`</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="ruiRoot" id="ruiRootView"></div>
|
||||||
|
<div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden;" onclick="clickOutsidePopup(event)"></div>
|
||||||
|
</body>
|
||||||
|
</html>`)
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) init(name, icon string) {
|
||||||
|
app.name = name
|
||||||
|
app.icon = icon
|
||||||
|
app.sessions = map[int]Session{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Start(addr string) {
|
||||||
|
http.Handle("/", app)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) Finish() {
|
||||||
|
for _, session := range app.sessions {
|
||||||
|
session.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) nextSessionID() int {
|
||||||
|
n := rand.Intn(0x7FFFFFFE) + 1
|
||||||
|
_, ok := app.sessions[n]
|
||||||
|
for ok {
|
||||||
|
n = rand.Intn(0x7FFFFFFE) + 1
|
||||||
|
_, ok = app.sessions[n]
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) removeSession(id int) {
|
||||||
|
delete(app.sessions, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
|
if ProtocolInDebugLog {
|
||||||
|
DebugLogF("%s %s", req.Method, req.URL.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch req.Method {
|
||||||
|
case "GET":
|
||||||
|
switch req.URL.Path {
|
||||||
|
case "/":
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
io.WriteString(w, app.getStartPage())
|
||||||
|
|
||||||
|
case "/ws":
|
||||||
|
if brige := CreateSocketBrige(w, req); brige != nil {
|
||||||
|
go app.socketReader(brige)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
filename := req.URL.Path[1:]
|
||||||
|
if size := len(filename); size > 0 && filename[size-1] == '/' {
|
||||||
|
filename = filename[:size-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if !serveResourceFile(filename, w, req) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) socketReader(brige WebBrige) {
|
||||||
|
var session Session
|
||||||
|
events := make(chan DataObject, 1024)
|
||||||
|
|
||||||
|
for {
|
||||||
|
message, ok := brige.ReadMessage()
|
||||||
|
if !ok {
|
||||||
|
events <- NewDataObject("disconnect")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ProtocolInDebugLog {
|
||||||
|
DebugLog(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj := ParseDataText(message); obj != nil {
|
||||||
|
command := obj.Tag()
|
||||||
|
switch command {
|
||||||
|
case "startSession":
|
||||||
|
answer := ""
|
||||||
|
if session, answer = app.startSession(obj, events, brige); session != nil {
|
||||||
|
if !brige.WriteMessage(answer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.onStart()
|
||||||
|
go sessionEventHandler(session, events, brige)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "reconnect":
|
||||||
|
if sessionText, ok := obj.PropertyValue("session"); ok {
|
||||||
|
if sessionID, err := strconv.Atoi(sessionText); err == nil {
|
||||||
|
if session = app.sessions[sessionID]; session != nil {
|
||||||
|
session.setBrige(events, brige)
|
||||||
|
answer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(answer)
|
||||||
|
|
||||||
|
session.writeInitScript(answer)
|
||||||
|
if !brige.WriteMessage(answer.String()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.onReconnect()
|
||||||
|
go sessionEventHandler(session, events, brige)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DebugLogF("Session #%d not exists", sessionID)
|
||||||
|
} else {
|
||||||
|
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(`"session" key not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
answer := ""
|
||||||
|
if session, answer = app.startSession(obj, events, brige); session != nil {
|
||||||
|
if !brige.WriteMessage(answer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.onStart()
|
||||||
|
go sessionEventHandler(session, events, brige)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "answer":
|
||||||
|
session.handleAnswer(obj)
|
||||||
|
|
||||||
|
case "imageLoaded":
|
||||||
|
session.imageManager().imageLoaded(obj, session)
|
||||||
|
|
||||||
|
case "imageError":
|
||||||
|
session.imageManager().imageLoadError(obj, session)
|
||||||
|
|
||||||
|
default:
|
||||||
|
events <- obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionEventHandler(session Session, events chan DataObject, brige WebBrige) {
|
||||||
|
for {
|
||||||
|
data := <-events
|
||||||
|
|
||||||
|
switch command := data.Tag(); command {
|
||||||
|
case "disconnect":
|
||||||
|
session.onDisconnect()
|
||||||
|
return
|
||||||
|
|
||||||
|
case "session-close":
|
||||||
|
session.onFinish()
|
||||||
|
session.App().removeSession(session.ID())
|
||||||
|
brige.Close()
|
||||||
|
|
||||||
|
case "session-pause":
|
||||||
|
session.onPause()
|
||||||
|
|
||||||
|
case "session-resume":
|
||||||
|
session.onResume()
|
||||||
|
|
||||||
|
case "resize":
|
||||||
|
session.handleResize(data)
|
||||||
|
|
||||||
|
default:
|
||||||
|
session.handleViewEvent(command, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) startSession(params DataObject, events chan DataObject, brige WebBrige) (Session, string) {
|
||||||
|
if app.createContentFunc == nil {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
session := newSession(app, app.nextSessionID(), "", params)
|
||||||
|
session.setBrige(events, brige)
|
||||||
|
if !session.setContent(app.createContentFunc(session), session) {
|
||||||
|
return nil, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
app.sessions[session.ID()] = session
|
||||||
|
|
||||||
|
answer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(answer)
|
||||||
|
|
||||||
|
answer.WriteString("sessionID = '")
|
||||||
|
answer.WriteString(strconv.Itoa(session.ID()))
|
||||||
|
answer.WriteString("';\n")
|
||||||
|
session.writeInitScript(answer)
|
||||||
|
answerText := answer.String()
|
||||||
|
|
||||||
|
if ProtocolInDebugLog {
|
||||||
|
DebugLog("Start session:")
|
||||||
|
DebugLog(answerText)
|
||||||
|
}
|
||||||
|
return session, answerText
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewApplication - create the new application of the single view type.
|
||||||
|
func NewApplication(name, icon string, createContentFunc func(Session) SessionContent) Application {
|
||||||
|
app := new(application)
|
||||||
|
app.init(name, icon)
|
||||||
|
app.createContentFunc = createContentFunc
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenBrowser(url string) bool {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
err = exec.Command("xdg-open", url).Start()
|
||||||
|
case "windows":
|
||||||
|
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||||
|
case "darwin":
|
||||||
|
err = exec.Command("open", url).Start()
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
return err != nil
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
type AudioPlayer interface {
|
||||||
|
MediaPlayer
|
||||||
|
}
|
||||||
|
|
||||||
|
type audioPlayerData struct {
|
||||||
|
mediaPlayerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAudioPlayer create new MediaPlayer object and return it
|
||||||
|
func NewAudioPlayer(session Session, params Params) MediaPlayer {
|
||||||
|
view := new(audioPlayerData)
|
||||||
|
view.Init(session)
|
||||||
|
view.tag = "AudioPlayer"
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAudioPlayer(session Session) View {
|
||||||
|
return NewAudioPlayer(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *audioPlayerData) Init(session Session) {
|
||||||
|
player.mediaPlayerData.Init(session)
|
||||||
|
player.tag = "AudioPlayer"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *audioPlayerData) htmlTag() string {
|
||||||
|
return "audio"
|
||||||
|
}
|
|
@ -0,0 +1,716 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NoRepeat is value of the Repeat property of an background image:
|
||||||
|
// The image is not repeated (and hence the background image painting area
|
||||||
|
// will not necessarily be entirely covered). The position of the non-repeated
|
||||||
|
// background image is defined by the background-position CSS property.
|
||||||
|
NoRepeat = 0
|
||||||
|
// RepeatXY is value of the Repeat property of an background image:
|
||||||
|
// The image is repeated as much as needed to cover the whole background
|
||||||
|
// image painting area. The last image will be clipped if it doesn't fit.
|
||||||
|
RepeatXY = 1
|
||||||
|
// RepeatX is value of the Repeat property of an background image:
|
||||||
|
// The image is repeated horizontally as much as needed to cover
|
||||||
|
// the whole width background image painting area. The image is not repeated vertically.
|
||||||
|
// The last image will be clipped if it doesn't fit.
|
||||||
|
RepeatX = 2
|
||||||
|
// RepeatY is value of the Repeat property of an background image:
|
||||||
|
// The image is repeated vertically as much as needed to cover
|
||||||
|
// the whole height background image painting area. The image is not repeated horizontally.
|
||||||
|
// The last image will be clipped if it doesn't fit.
|
||||||
|
RepeatY = 3
|
||||||
|
// RepeatRound is value of the Repeat property of an background image:
|
||||||
|
// As the allowed space increases in size, the repeated images will stretch (leaving no gaps)
|
||||||
|
// until there is room (space left >= half of the image width) for another one to be added.
|
||||||
|
// When the next image is added, all of the current ones compress to allow room.
|
||||||
|
RepeatRound = 4
|
||||||
|
// RepeatSpace is value of the Repeat property of an background image:
|
||||||
|
// The image is repeated as much as possible without clipping. The first and last images
|
||||||
|
// are pinned to either side of the element, and whitespace is distributed evenly between the images.
|
||||||
|
RepeatSpace = 5
|
||||||
|
|
||||||
|
// ScrollAttachment is value of the Attachment property of an background image:
|
||||||
|
// The background is fixed relative to the element itself and does not scroll with its contents.
|
||||||
|
// (It is effectively attached to the element's border.)
|
||||||
|
ScrollAttachment = 0
|
||||||
|
// FixedAttachment is value of the Attachment property of an background image:
|
||||||
|
// The background is fixed relative to the viewport. Even if an element has
|
||||||
|
// a scrolling mechanism, the background doesn't move with the element.
|
||||||
|
FixedAttachment = 1
|
||||||
|
// LocalAttachment is value of the Attachment property of an background image:
|
||||||
|
// The background is fixed relative to the element's contents. If the element has a scrolling mechanism,
|
||||||
|
// the background scrolls with the element's contents, and the background painting area
|
||||||
|
// and background positioning area are relative to the scrollable area of the element
|
||||||
|
// rather than to the border framing them.
|
||||||
|
LocalAttachment = 2
|
||||||
|
|
||||||
|
// BorderBoxClip is value of the BackgroundClip property:
|
||||||
|
// The background extends to the outside edge of the border (but underneath the border in z-ordering).
|
||||||
|
BorderBoxClip = 0
|
||||||
|
// PaddingBoxClip is value of the BackgroundClip property:
|
||||||
|
// The background extends to the outside edge of the padding. No background is drawn beneath the border.
|
||||||
|
PaddingBoxClip = 1
|
||||||
|
// ContentBoxClip is value of the BackgroundClip property:
|
||||||
|
// The background is painted within (clipped to) the content box.
|
||||||
|
ContentBoxClip = 2
|
||||||
|
|
||||||
|
// ToTopGradient is value of the Direction property of a linear gradient. The value is equivalent to the 0deg angle
|
||||||
|
ToTopGradient = 0
|
||||||
|
// ToRightTopGradient is value of the Direction property of a linear gradient.
|
||||||
|
ToRightTopGradient = 1
|
||||||
|
// ToRightGradient is value of the Direction property of a linear gradient. The value is equivalent to the 90deg angle
|
||||||
|
ToRightGradient = 2
|
||||||
|
// ToRightBottomGradient is value of the Direction property of a linear gradient.
|
||||||
|
ToRightBottomGradient = 3
|
||||||
|
// ToBottomGradient is value of the Direction property of a linear gradient. The value is equivalent to the 180deg angle
|
||||||
|
ToBottomGradient = 4
|
||||||
|
// ToLeftBottomGradient is value of the Direction property of a linear gradient.
|
||||||
|
ToLeftBottomGradient = 5
|
||||||
|
// ToLeftGradient is value of the Direction property of a linear gradient. The value is equivalent to the 270deg angle
|
||||||
|
ToLeftGradient = 6
|
||||||
|
// ToLeftTopGradient is value of the Direction property of a linear gradient.
|
||||||
|
ToLeftTopGradient = 7
|
||||||
|
|
||||||
|
// EllipseGradient is value of the Shape property of a radial gradient background:
|
||||||
|
// the shape is an axis-aligned ellipse
|
||||||
|
EllipseGradient = 0
|
||||||
|
// CircleGradient is value of the Shape property of a radial gradient background:
|
||||||
|
// the gradient's shape is a circle with constant radius
|
||||||
|
CircleGradient = 1
|
||||||
|
|
||||||
|
// ClosestSideGradient is value of the Radius property of a radial gradient background:
|
||||||
|
// The gradient's ending shape meets the side of the box closest to its center (for circles)
|
||||||
|
// or meets both the vertical and horizontal sides closest to the center (for ellipses).
|
||||||
|
ClosestSideGradient = 0
|
||||||
|
// ClosestCornerGradient is value of the Radius property of a radial gradient background:
|
||||||
|
// The gradient's ending shape is sized so that it exactly meets the closest corner
|
||||||
|
// of the box from its center.
|
||||||
|
ClosestCornerGradient = 1
|
||||||
|
// FarthestSideGradient is value of the Radius property of a radial gradient background:
|
||||||
|
// Similar to closest-side, except the ending shape is sized to meet the side of the box
|
||||||
|
// farthest from its center (or vertical and horizontal sides).
|
||||||
|
FarthestSideGradient = 2
|
||||||
|
// FarthestCornerGradient is value of the Radius property of a radial gradient background:
|
||||||
|
// The default value, the gradient's ending shape is sized so that it exactly meets
|
||||||
|
// the farthest corner of the box from its center.
|
||||||
|
FarthestCornerGradient = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackgroundElement describes the background element.
|
||||||
|
type BackgroundElement interface {
|
||||||
|
Properties
|
||||||
|
cssStyle(view View) string
|
||||||
|
Tag() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type backgroundElement struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
type backgroundImage struct {
|
||||||
|
backgroundElement
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackgroundGradientPoint define point on gradient straight line
|
||||||
|
type BackgroundGradientPoint struct {
|
||||||
|
// Pos - the distance from the start of the gradient straight line
|
||||||
|
Pos SizeUnit
|
||||||
|
// Color - the color of the point
|
||||||
|
Color Color
|
||||||
|
}
|
||||||
|
|
||||||
|
type backgroundGradient struct {
|
||||||
|
backgroundElement
|
||||||
|
}
|
||||||
|
|
||||||
|
type backgroundLinearGradient struct {
|
||||||
|
backgroundGradient
|
||||||
|
}
|
||||||
|
|
||||||
|
type backgroundRadialGradient struct {
|
||||||
|
backgroundGradient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackgroundImage creates the new background image
|
||||||
|
func createBackground(obj DataObject) BackgroundElement {
|
||||||
|
var result BackgroundElement = nil
|
||||||
|
|
||||||
|
switch obj.Tag() {
|
||||||
|
case "image":
|
||||||
|
image := new(backgroundImage)
|
||||||
|
image.properties = map[string]interface{}{}
|
||||||
|
result = image
|
||||||
|
|
||||||
|
case "linear-gradient":
|
||||||
|
gradient := new(backgroundLinearGradient)
|
||||||
|
gradient.properties = map[string]interface{}{}
|
||||||
|
result = gradient
|
||||||
|
|
||||||
|
case "radial-gradient":
|
||||||
|
gradient := new(backgroundRadialGradient)
|
||||||
|
gradient.properties = map[string]interface{}{}
|
||||||
|
result = gradient
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
count := obj.PropertyCount()
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
if node := obj.Property(i); node.Type() == TextNode {
|
||||||
|
if value := node.Text(); value != "" {
|
||||||
|
result.Set(node.Tag(), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackgroundImage creates the new background image
|
||||||
|
func NewBackgroundImage(params Params) BackgroundElement {
|
||||||
|
result := new(backgroundImage)
|
||||||
|
result.properties = map[string]interface{}{}
|
||||||
|
for tag, value := range params {
|
||||||
|
result.Set(tag, value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackgroundLinearGradient creates the new background linear gradient
|
||||||
|
func NewBackgroundLinearGradient(params Params) BackgroundElement {
|
||||||
|
result := new(backgroundLinearGradient)
|
||||||
|
result.properties = map[string]interface{}{}
|
||||||
|
for tag, value := range params {
|
||||||
|
result.Set(tag, value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBackgroundRadialGradient creates the new background radial gradient
|
||||||
|
func NewBackgroundRadialGradient(params Params) BackgroundElement {
|
||||||
|
result := new(backgroundRadialGradient)
|
||||||
|
result.properties = map[string]interface{}{}
|
||||||
|
for tag, value := range params {
|
||||||
|
result.Set(tag, value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *backgroundImage) Tag() string {
|
||||||
|
return "image"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *backgroundImage) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case "source":
|
||||||
|
tag = Source
|
||||||
|
|
||||||
|
case Fit:
|
||||||
|
tag = backgroundFit
|
||||||
|
|
||||||
|
case HorizontalAlign:
|
||||||
|
tag = ImageHorizontalAlign
|
||||||
|
|
||||||
|
case VerticalAlign:
|
||||||
|
tag = ImageVerticalAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *backgroundImage) Set(tag string, value interface{}) bool {
|
||||||
|
tag = image.normalizeTag(tag)
|
||||||
|
switch tag {
|
||||||
|
case Attachment, Width, Height, Repeat, ImageHorizontalAlign, ImageVerticalAlign,
|
||||||
|
backgroundFit, Source:
|
||||||
|
return image.backgroundElement.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *backgroundImage) Get(tag string) interface{} {
|
||||||
|
return image.backgroundElement.Get(image.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *backgroundImage) cssStyle(view View) string {
|
||||||
|
session := view.Session()
|
||||||
|
if src, ok := stringProperty(image, Source, session); ok && src != "" {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(`url(`)
|
||||||
|
buffer.WriteString(src)
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
|
||||||
|
attachment, _ := enumProperty(image, Attachment, session, NoRepeat)
|
||||||
|
values := enumProperties[Attachment].values
|
||||||
|
if attachment > 0 && attachment < len(values) {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(values[attachment])
|
||||||
|
}
|
||||||
|
|
||||||
|
align, _ := enumProperty(image, ImageHorizontalAlign, session, LeftAlign)
|
||||||
|
values = enumProperties[ImageHorizontalAlign].values
|
||||||
|
if align >= 0 && align < len(values) {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(values[align])
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` left`)
|
||||||
|
}
|
||||||
|
|
||||||
|
align, _ = enumProperty(image, ImageVerticalAlign, session, TopAlign)
|
||||||
|
values = enumProperties[ImageVerticalAlign].values
|
||||||
|
if align >= 0 && align < len(values) {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(values[align])
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` top`)
|
||||||
|
}
|
||||||
|
|
||||||
|
fit, _ := enumProperty(image, backgroundFit, session, NoneFit)
|
||||||
|
values = enumProperties[backgroundFit].values
|
||||||
|
if fit > 0 && fit < len(values) {
|
||||||
|
|
||||||
|
buffer.WriteString(` / `)
|
||||||
|
buffer.WriteString(values[fit])
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
width, _ := sizeProperty(image, Width, session)
|
||||||
|
height, _ := sizeProperty(image, Height, session)
|
||||||
|
|
||||||
|
if width.Type != Auto || height.Type != Auto {
|
||||||
|
buffer.WriteString(` / `)
|
||||||
|
buffer.WriteString(width.cssString("auto"))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(height.cssString("auto"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repeat, _ := enumProperty(image, Repeat, session, NoRepeat)
|
||||||
|
values = enumProperties[Repeat].values
|
||||||
|
if repeat >= 0 && repeat < len(values) {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(values[repeat])
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` no-repeat`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundGradient) Set(tag string, value interface{}) bool {
|
||||||
|
|
||||||
|
switch tag = strings.ToLower(tag); tag {
|
||||||
|
case Repeat:
|
||||||
|
return gradient.setBoolProperty(tag, value)
|
||||||
|
|
||||||
|
case Gradient:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if value != "" {
|
||||||
|
elements := strings.Split(value, `,`)
|
||||||
|
if count := len(elements); count > 1 {
|
||||||
|
points := make([]interface{}, count)
|
||||||
|
for i, element := range elements {
|
||||||
|
if strings.Contains(element, "@") {
|
||||||
|
points[i] = element
|
||||||
|
} else {
|
||||||
|
var point BackgroundGradientPoint
|
||||||
|
if point.setValue(element) {
|
||||||
|
points[i] = point
|
||||||
|
} else {
|
||||||
|
ErrorLogF("Invalid gradient element #%d: %s", i, element)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gradient.properties[Gradient] = points
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
text := strings.Trim(value, " \n\r\t")
|
||||||
|
if text[0] == '@' {
|
||||||
|
gradient.properties[Gradient] = text
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case []BackgroundGradientPoint:
|
||||||
|
if len(value) >= 2 {
|
||||||
|
gradient.properties[Gradient] = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case []Color:
|
||||||
|
count := len(value)
|
||||||
|
if count >= 2 {
|
||||||
|
points := make([]BackgroundGradientPoint, count)
|
||||||
|
for i, color := range value {
|
||||||
|
points[i].Color = color
|
||||||
|
points[i].Pos = AutoSize()
|
||||||
|
}
|
||||||
|
gradient.properties[Gradient] = points
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case []GradientPoint:
|
||||||
|
count := len(value)
|
||||||
|
if count >= 2 {
|
||||||
|
points := make([]BackgroundGradientPoint, count)
|
||||||
|
for i, point := range value {
|
||||||
|
points[i].Color = point.Color
|
||||||
|
points[i].Pos = Percent(point.Offset * 100)
|
||||||
|
}
|
||||||
|
gradient.properties[Gradient] = points
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
if count := len(value); count > 1 {
|
||||||
|
points := make([]interface{}, count)
|
||||||
|
for i, element := range value {
|
||||||
|
switch element := element.(type) {
|
||||||
|
case string:
|
||||||
|
if strings.Contains(element, "@") {
|
||||||
|
points[i] = element
|
||||||
|
} else {
|
||||||
|
var point BackgroundGradientPoint
|
||||||
|
if !point.setValue(element) {
|
||||||
|
ErrorLogF("Invalid gradient element #%d: %s", i, element)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
points[i] = point
|
||||||
|
}
|
||||||
|
|
||||||
|
case BackgroundGradientPoint:
|
||||||
|
points[i] = element
|
||||||
|
|
||||||
|
case GradientPoint:
|
||||||
|
points[i] = BackgroundGradientPoint{Color: element.Color, Pos: Percent(element.Offset * 100)}
|
||||||
|
|
||||||
|
case Color:
|
||||||
|
points[i] = BackgroundGradientPoint{Color: element, Pos: AutoSize()}
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF("Invalid gradient element #%d: %v", i, element)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gradient.properties[Gradient] = points
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF("Invalid gradient %v", value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return gradient.backgroundElement.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (point *BackgroundGradientPoint) setValue(value string) bool {
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
switch elements := strings.Split(value, `:`); len(elements) {
|
||||||
|
case 2:
|
||||||
|
if point.Color, ok = StringToColor(elements[0]); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if point.Pos, ok = StringToSizeUnit(elements[1]); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
if point.Color, ok = StringToColor(elements[0]); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
point.Pos = AutoSize()
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundGradient) writeGradient(view View, buffer *strings.Builder) bool {
|
||||||
|
|
||||||
|
value, ok := gradient.properties[Gradient]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
points := []BackgroundGradientPoint{}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if text, ok := view.Session().resolveConstants(value); ok && text != "" {
|
||||||
|
elements := strings.Split(text, `,`)
|
||||||
|
points := make([]BackgroundGradientPoint, len(elements))
|
||||||
|
for i, element := range elements {
|
||||||
|
if !points[i].setValue(element) {
|
||||||
|
ErrorLogF(`Invalid gradient point #%d: "%s"`, i, element)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(`Invalid gradient: ` + value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case []BackgroundGradientPoint:
|
||||||
|
points = value
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
points = make([]BackgroundGradientPoint, len(value))
|
||||||
|
for i, element := range value {
|
||||||
|
switch element := element.(type) {
|
||||||
|
case string:
|
||||||
|
if text, ok := view.Session().resolveConstants(element); ok && text != "" {
|
||||||
|
if !points[i].setValue(text) {
|
||||||
|
ErrorLogF(`Invalid gradient point #%d: "%s"`, i, text)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`Invalid gradient point #%d: "%s"`, i, text)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case BackgroundGradientPoint:
|
||||||
|
points[i] = element
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(points) > 0 {
|
||||||
|
for i, point := range points {
|
||||||
|
if i > 0 {
|
||||||
|
buffer.WriteString(`, `)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(point.Color.cssString())
|
||||||
|
if point.Pos.Type != Auto {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(point.Pos.cssString(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundLinearGradient) Tag() string {
|
||||||
|
return "linear-gradient"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundLinearGradient) Set(tag string, value interface{}) bool {
|
||||||
|
if tag == Direction {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case AngleUnit:
|
||||||
|
gradient.properties[Direction] = value
|
||||||
|
return true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
var angle AngleUnit
|
||||||
|
if ok, _ := angle.setValue(value); ok {
|
||||||
|
gradient.properties[Direction] = angle
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gradient.setEnumProperty(tag, value, enumProperties[Direction].values)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gradient.backgroundGradient.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundLinearGradient) cssStyle(view View) string {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
session := view.Session()
|
||||||
|
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
|
||||||
|
buffer.WriteString(`repeating-linear-gradient(`)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(`linear-gradient(`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := gradient.properties[Direction]; ok {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
direction := enumProperties[Direction]
|
||||||
|
if n, ok := enumStringToInt(text, direction.values, false); ok {
|
||||||
|
buffer.WriteString(direction.cssValues[n])
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
} else {
|
||||||
|
if angle, ok := StringToAngleUnit(text); ok {
|
||||||
|
buffer.WriteString(angle.cssString())
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
} else {
|
||||||
|
ErrorLog(`Invalid linear gradient direction: ` + text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(`Invalid linear gradient direction: ` + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case int:
|
||||||
|
values := enumProperties[Direction].cssValues
|
||||||
|
if value >= 0 && value < len(values) {
|
||||||
|
buffer.WriteString(values[value])
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`Invalid linear gradient direction: %d`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case AngleUnit:
|
||||||
|
buffer.WriteString(value.cssString())
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !gradient.writeGradient(view, buffer) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(") ")
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundRadialGradient) Tag() string {
|
||||||
|
return "radial-gradient"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundRadialGradient) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case Radius:
|
||||||
|
tag = RadialGradientRadius
|
||||||
|
|
||||||
|
case Shape:
|
||||||
|
tag = RadialGradientShape
|
||||||
|
|
||||||
|
case "x-center":
|
||||||
|
tag = CenterX
|
||||||
|
|
||||||
|
case "y-center":
|
||||||
|
tag = CenterY
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundRadialGradient) Set(tag string, value interface{}) bool {
|
||||||
|
tag = gradient.normalizeTag(tag)
|
||||||
|
switch tag {
|
||||||
|
case RadialGradientRadius:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string, SizeUnit:
|
||||||
|
return gradient.propertyList.Set(RadialGradientRadius, value)
|
||||||
|
|
||||||
|
case int:
|
||||||
|
n := value
|
||||||
|
if n >= 0 && n < len(enumProperties[RadialGradientRadius].values) {
|
||||||
|
return gradient.propertyList.Set(RadialGradientRadius, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrorLogF(`Invalid value of "%s" property: %v`, tag, value)
|
||||||
|
|
||||||
|
case RadialGradientShape:
|
||||||
|
return gradient.propertyList.Set(RadialGradientShape, value)
|
||||||
|
|
||||||
|
case CenterX, CenterY:
|
||||||
|
return gradient.propertyList.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return gradient.backgroundGradient.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundRadialGradient) Get(tag string) interface{} {
|
||||||
|
return gradient.backgroundGradient.Get(gradient.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gradient *backgroundRadialGradient) cssStyle(view View) string {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
session := view.Session()
|
||||||
|
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
|
||||||
|
buffer.WriteString(`repeating-radial-gradient(`)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(`radial-gradient(`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if shape, ok := enumProperty(gradient, RadialGradientShape, session, EllipseGradient); ok && shape == CircleGradient {
|
||||||
|
buffer.WriteString(`circle `)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(`ellipse `)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := gradient.properties[RadialGradientRadius]; ok {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
values := enumProperties[RadialGradientRadius]
|
||||||
|
if n, ok := enumStringToInt(text, values.values, false); ok {
|
||||||
|
buffer.WriteString(values.cssValues[n])
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
} else {
|
||||||
|
if r, ok := StringToSizeUnit(text); ok && r.Type != Auto {
|
||||||
|
buffer.WriteString(r.cssString(""))
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
} else {
|
||||||
|
ErrorLog(`Invalid linear gradient radius: ` + text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(`Invalid linear gradient radius: ` + value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case int:
|
||||||
|
values := enumProperties[RadialGradientRadius].cssValues
|
||||||
|
if value >= 0 && value < len(values) {
|
||||||
|
buffer.WriteString(values[value])
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`Invalid linear gradient radius: %d`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
if value.Type != Auto {
|
||||||
|
buffer.WriteString(value.cssString(""))
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x, _ := sizeProperty(gradient, CenterX, session)
|
||||||
|
y, _ := sizeProperty(gradient, CenterX, session)
|
||||||
|
if x.Type != Auto || y.Type != Auto {
|
||||||
|
buffer.WriteString("at ")
|
||||||
|
buffer.WriteString(x.cssString("50%"))
|
||||||
|
buffer.WriteString(" ")
|
||||||
|
buffer.WriteString(y.cssString("50%"))
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
if !gradient.writeGradient(view, buffer) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(") ")
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
|
@ -0,0 +1,710 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NoneLine constant specifies that there is no border
|
||||||
|
NoneLine = 0
|
||||||
|
// SolidLine constant specifies the border/line as a solid line
|
||||||
|
SolidLine = 1
|
||||||
|
// DashedLine constant specifies the border/line as a dashed line
|
||||||
|
DashedLine = 2
|
||||||
|
// DottedLine constant specifies the border/line as a dotted line
|
||||||
|
DottedLine = 3
|
||||||
|
// DoubleLine constant specifies the border/line as a double solid line
|
||||||
|
DoubleLine = 4
|
||||||
|
// DoubleLine constant specifies the border/line as a double solid line
|
||||||
|
WavyLine = 5
|
||||||
|
|
||||||
|
// LeftStyle is the constant for "left-style" property tag
|
||||||
|
LeftStyle = "left-style"
|
||||||
|
// RightStyle is the constant for "-right-style" property tag
|
||||||
|
RightStyle = "right-style"
|
||||||
|
// TopStyle is the constant for "top-style" property tag
|
||||||
|
TopStyle = "top-style"
|
||||||
|
// BottomStyle is the constant for "bottom-style" property tag
|
||||||
|
BottomStyle = "bottom-style"
|
||||||
|
// LeftWidth is the constant for "left-width" property tag
|
||||||
|
LeftWidth = "left-width"
|
||||||
|
// RightWidth is the constant for "-right-width" property tag
|
||||||
|
RightWidth = "right-width"
|
||||||
|
// TopWidth is the constant for "top-width" property tag
|
||||||
|
TopWidth = "top-width"
|
||||||
|
// BottomWidth is the constant for "bottom-width" property tag
|
||||||
|
BottomWidth = "bottom-width"
|
||||||
|
// LeftColor is the constant for "left-color" property tag
|
||||||
|
LeftColor = "left-color"
|
||||||
|
// RightColor is the constant for "-right-color" property tag
|
||||||
|
RightColor = "right-color"
|
||||||
|
// TopColor is the constant for "top-color" property tag
|
||||||
|
TopColor = "top-color"
|
||||||
|
// BottomColor is the constant for "bottom-color" property tag
|
||||||
|
BottomColor = "bottom-color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BorderProperty is the interface of a view border data
|
||||||
|
type BorderProperty interface {
|
||||||
|
Properties
|
||||||
|
ruiStringer
|
||||||
|
fmt.Stringer
|
||||||
|
ViewBorders(session Session) ViewBorders
|
||||||
|
delete(tag string)
|
||||||
|
cssStyle(builder cssBuilder, session Session)
|
||||||
|
cssWidth(builder cssBuilder, session Session)
|
||||||
|
cssColor(builder cssBuilder, session Session)
|
||||||
|
cssStyleValue(session Session) string
|
||||||
|
cssWidthValue(session Session) string
|
||||||
|
cssColorValue(session Session) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type borderProperty struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBorderProperty(value interface{}) BorderProperty {
|
||||||
|
border := new(borderProperty)
|
||||||
|
border.properties = map[string]interface{}{}
|
||||||
|
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case BorderProperty:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
_ = border.setBorderObject(value)
|
||||||
|
|
||||||
|
case ViewBorder:
|
||||||
|
border.properties[Style] = value.Style
|
||||||
|
border.properties[Width] = value.Width
|
||||||
|
border.properties[ColorProperty] = value.Color
|
||||||
|
|
||||||
|
case ViewBorders:
|
||||||
|
if value.Left.Style == value.Right.Style &&
|
||||||
|
value.Left.Style == value.Top.Style &&
|
||||||
|
value.Left.Style == value.Bottom.Style {
|
||||||
|
border.properties[Style] = value.Left.Style
|
||||||
|
} else {
|
||||||
|
border.properties[LeftStyle] = value.Left.Style
|
||||||
|
border.properties[RightStyle] = value.Right.Style
|
||||||
|
border.properties[TopStyle] = value.Top.Style
|
||||||
|
border.properties[BottomStyle] = value.Bottom.Style
|
||||||
|
}
|
||||||
|
if value.Left.Width.Equal(value.Right.Width) &&
|
||||||
|
value.Left.Width.Equal(value.Top.Width) &&
|
||||||
|
value.Left.Width.Equal(value.Bottom.Width) {
|
||||||
|
border.properties[Width] = value.Left.Width
|
||||||
|
} else {
|
||||||
|
border.properties[LeftWidth] = value.Left.Width
|
||||||
|
border.properties[RightWidth] = value.Right.Width
|
||||||
|
border.properties[TopWidth] = value.Top.Width
|
||||||
|
border.properties[BottomWidth] = value.Bottom.Width
|
||||||
|
}
|
||||||
|
if value.Left.Color == value.Right.Color &&
|
||||||
|
value.Left.Color == value.Top.Color &&
|
||||||
|
value.Left.Color == value.Bottom.Color {
|
||||||
|
border.properties[ColorProperty] = value.Left.Color
|
||||||
|
} else {
|
||||||
|
border.properties[LeftColor] = value.Left.Color
|
||||||
|
border.properties[RightColor] = value.Right.Color
|
||||||
|
border.properties[TopColor] = value.Top.Color
|
||||||
|
border.properties[BottomColor] = value.Bottom.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
invalidPropertyValue(Border, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return border
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBorder creates the new BorderProperty
|
||||||
|
func NewBorder(params Params) BorderProperty {
|
||||||
|
border := new(borderProperty)
|
||||||
|
border.properties = map[string]interface{}{}
|
||||||
|
if params != nil {
|
||||||
|
for _, tag := range []string{Style, Width, ColorProperty, Left, Right, Top, Bottom,
|
||||||
|
LeftStyle, RightStyle, TopStyle, BottomStyle,
|
||||||
|
LeftWidth, RightWidth, TopWidth, BottomWidth,
|
||||||
|
LeftColor, RightColor, TopColor, BottomColor} {
|
||||||
|
if value, ok := params[tag]; ok && value != nil {
|
||||||
|
border.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return border
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case BorderLeft, CellBorderLeft:
|
||||||
|
return Left
|
||||||
|
|
||||||
|
case BorderRight, CellBorderRight:
|
||||||
|
return Right
|
||||||
|
|
||||||
|
case BorderTop, CellBorderTop:
|
||||||
|
return Top
|
||||||
|
|
||||||
|
case BorderBottom, CellBorderBottom:
|
||||||
|
return Bottom
|
||||||
|
|
||||||
|
case BorderStyle, CellBorderStyle:
|
||||||
|
return Style
|
||||||
|
|
||||||
|
case BorderLeftStyle, CellBorderLeftStyle, "style-left":
|
||||||
|
return LeftStyle
|
||||||
|
|
||||||
|
case BorderRightStyle, CellBorderRightStyle, "style-right":
|
||||||
|
return RightStyle
|
||||||
|
|
||||||
|
case BorderTopStyle, CellBorderTopStyle, "style-top":
|
||||||
|
return TopStyle
|
||||||
|
|
||||||
|
case BorderBottomStyle, CellBorderBottomStyle, "style-bottom":
|
||||||
|
return BottomStyle
|
||||||
|
|
||||||
|
case BorderWidth, CellBorderWidth:
|
||||||
|
return Width
|
||||||
|
|
||||||
|
case BorderLeftWidth, CellBorderLeftWidth, "width-left":
|
||||||
|
return LeftWidth
|
||||||
|
|
||||||
|
case BorderRightWidth, CellBorderRightWidth, "width-right":
|
||||||
|
return RightWidth
|
||||||
|
|
||||||
|
case BorderTopWidth, CellBorderTopWidth, "width-top":
|
||||||
|
return TopWidth
|
||||||
|
|
||||||
|
case BorderBottomWidth, CellBorderBottomWidth, "width-bottom":
|
||||||
|
return BottomWidth
|
||||||
|
|
||||||
|
case BorderColor, CellBorderColor:
|
||||||
|
return ColorProperty
|
||||||
|
|
||||||
|
case BorderLeftColor, CellBorderLeftColor, "color-left":
|
||||||
|
return LeftColor
|
||||||
|
|
||||||
|
case BorderRightColor, CellBorderRightColor, "color-right":
|
||||||
|
return RightColor
|
||||||
|
|
||||||
|
case BorderTopColor, CellBorderTopColor, "color-top":
|
||||||
|
return TopColor
|
||||||
|
|
||||||
|
case BorderBottomColor, CellBorderBottomColor, "color-bottom":
|
||||||
|
return BottomColor
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) ruiString(writer ruiWriter) {
|
||||||
|
writer.startObject("_")
|
||||||
|
|
||||||
|
for _, tag := range []string{Style, Width, ColorProperty} {
|
||||||
|
if value, ok := border.properties[tag]; ok {
|
||||||
|
writer.writeProperty(Style, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, side := range []string{Top, Right, Bottom, Left} {
|
||||||
|
style, okStyle := border.properties[side+"-"+Style]
|
||||||
|
width, okWidth := border.properties[side+"-"+Width]
|
||||||
|
color, okColor := border.properties[side+"-"+ColorProperty]
|
||||||
|
if okStyle || okWidth || okColor {
|
||||||
|
writer.startObjectProperty(side, "_")
|
||||||
|
if okStyle {
|
||||||
|
writer.writeProperty(Style, style)
|
||||||
|
}
|
||||||
|
if okWidth {
|
||||||
|
writer.writeProperty(Width, width)
|
||||||
|
}
|
||||||
|
if okColor {
|
||||||
|
writer.writeProperty(ColorProperty, color)
|
||||||
|
}
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
border.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) setSingleBorderObject(prefix string, obj DataObject) bool {
|
||||||
|
result := true
|
||||||
|
if text, ok := obj.PropertyValue(Style); ok {
|
||||||
|
if !border.setEnumProperty(prefix+"-style", text, enumProperties[BorderStyle].values) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if text, ok := obj.PropertyValue(ColorProperty); ok {
|
||||||
|
if !border.setColorProperty(prefix+"-color", text) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if text, ok := obj.PropertyValue("width"); ok {
|
||||||
|
if !border.setSizeProperty(prefix+"-width", text) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) setBorderObject(obj DataObject) bool {
|
||||||
|
result := true
|
||||||
|
|
||||||
|
for _, side := range []string{Top, Right, Bottom, Left} {
|
||||||
|
if node := obj.PropertyWithTag(side); node != nil {
|
||||||
|
if node.Type() == ObjectNode {
|
||||||
|
if !border.setSingleBorderObject(side, node.Object()) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notCompatibleType(side, node)
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if text, ok := obj.PropertyValue(Style); ok {
|
||||||
|
values := split4Values(text)
|
||||||
|
styles := enumProperties[BorderStyle].values
|
||||||
|
switch len(values) {
|
||||||
|
case 1:
|
||||||
|
if !border.setEnumProperty(Style, values[0], styles) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
for n, tag := range [4]string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
|
||||||
|
if !border.setEnumProperty(tag, values[n], styles) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(Style, text)
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if text, ok := obj.PropertyValue(ColorProperty); ok {
|
||||||
|
values := split4Values(text)
|
||||||
|
switch len(values) {
|
||||||
|
case 1:
|
||||||
|
if !border.setColorProperty(ColorProperty, values[0]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
for n, tag := range [4]string{TopColor, RightColor, BottomColor, LeftColor} {
|
||||||
|
if !border.setColorProperty(tag, values[n]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(ColorProperty, text)
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if text, ok := obj.PropertyValue(Width); ok {
|
||||||
|
values := split4Values(text)
|
||||||
|
switch len(values) {
|
||||||
|
case 1:
|
||||||
|
if !border.setSizeProperty(Width, values[0]) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
for n, tag := range [4]string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
|
||||||
|
if !border.setSizeProperty(tag, values[n]) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(Width, text)
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) Remove(tag string) {
|
||||||
|
tag = border.normalizeTag(tag)
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Style:
|
||||||
|
for _, t := range []string{tag, TopStyle, RightStyle, BottomStyle, LeftStyle} {
|
||||||
|
delete(border.properties, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Width:
|
||||||
|
for _, t := range []string{tag, TopWidth, RightWidth, BottomWidth, LeftWidth} {
|
||||||
|
delete(border.properties, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
case ColorProperty:
|
||||||
|
for _, t := range []string{tag, TopColor, RightColor, BottomColor, LeftColor} {
|
||||||
|
delete(border.properties, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Left, Right, Top, Bottom:
|
||||||
|
border.Remove(tag + "-style")
|
||||||
|
border.Remove(tag + "-width")
|
||||||
|
border.Remove(tag + "-color")
|
||||||
|
|
||||||
|
case LeftStyle, RightStyle, TopStyle, BottomStyle:
|
||||||
|
delete(border.properties, tag)
|
||||||
|
if style, ok := border.properties[Style]; ok && style != nil {
|
||||||
|
for _, t := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
|
||||||
|
if t != tag {
|
||||||
|
if _, ok := border.properties[t]; !ok {
|
||||||
|
border.properties[t] = style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case LeftWidth, RightWidth, TopWidth, BottomWidth:
|
||||||
|
delete(border.properties, tag)
|
||||||
|
if width, ok := border.properties[Width]; ok && width != nil {
|
||||||
|
for _, t := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
|
||||||
|
if t != tag {
|
||||||
|
if _, ok := border.properties[t]; !ok {
|
||||||
|
border.properties[t] = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case LeftColor, RightColor, TopColor, BottomColor:
|
||||||
|
delete(border.properties, tag)
|
||||||
|
if color, ok := border.properties[ColorProperty]; ok && color != nil {
|
||||||
|
for _, t := range []string{TopColor, RightColor, BottomColor, LeftColor} {
|
||||||
|
if t != tag {
|
||||||
|
if _, ok := border.properties[t]; !ok {
|
||||||
|
border.properties[t] = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) Set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
border.Remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = border.normalizeTag(tag)
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Style:
|
||||||
|
if border.setEnumProperty(Style, value, enumProperties[BorderStyle].values) {
|
||||||
|
for _, side := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
|
||||||
|
delete(border.properties, side)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case Width:
|
||||||
|
if border.setSizeProperty(Width, value) {
|
||||||
|
for _, side := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
|
||||||
|
delete(border.properties, side)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case ColorProperty:
|
||||||
|
if border.setColorProperty(ColorProperty, value) {
|
||||||
|
for _, side := range []string{TopColor, RightColor, BottomColor, LeftColor} {
|
||||||
|
delete(border.properties, side)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case LeftStyle, RightStyle, TopStyle, BottomStyle:
|
||||||
|
return border.setEnumProperty(tag, value, enumProperties[BorderStyle].values)
|
||||||
|
|
||||||
|
case LeftWidth, RightWidth, TopWidth, BottomWidth:
|
||||||
|
return border.setSizeProperty(tag, value)
|
||||||
|
|
||||||
|
case LeftColor, RightColor, TopColor, BottomColor:
|
||||||
|
return border.setColorProperty(tag, value)
|
||||||
|
|
||||||
|
case Left, Right, Top, Bottom:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if obj := ParseDataText(value); obj != nil {
|
||||||
|
return border.setSingleBorderObject(tag, obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
return border.setSingleBorderObject(tag, value)
|
||||||
|
|
||||||
|
case BorderProperty:
|
||||||
|
styleTag := tag + "-" + Style
|
||||||
|
if style := value.Get(styleTag); value != nil {
|
||||||
|
border.properties[styleTag] = style
|
||||||
|
}
|
||||||
|
colorTag := tag + "-" + ColorProperty
|
||||||
|
if color := value.Get(colorTag); value != nil {
|
||||||
|
border.properties[colorTag] = color
|
||||||
|
}
|
||||||
|
widthTag := tag + "-" + Width
|
||||||
|
if width := value.Get(widthTag); value != nil {
|
||||||
|
border.properties[widthTag] = width
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case ViewBorder:
|
||||||
|
border.properties[tag+"-"+Style] = value.Style
|
||||||
|
border.properties[tag+"-"+Width] = value.Width
|
||||||
|
border.properties[tag+"-"+ColorProperty] = value.Color
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) Get(tag string) interface{} {
|
||||||
|
tag = border.normalizeTag(tag)
|
||||||
|
|
||||||
|
if result, ok := border.properties[tag]; ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Left, Right, Top, Bottom:
|
||||||
|
result := newBorderProperty(nil)
|
||||||
|
if style, ok := border.properties[tag+"-"+Style]; ok {
|
||||||
|
result.Set(Style, style)
|
||||||
|
} else if style, ok := border.properties[Style]; ok {
|
||||||
|
result.Set(Style, style)
|
||||||
|
}
|
||||||
|
if width, ok := border.properties[tag+"-"+Width]; ok {
|
||||||
|
result.Set(Width, width)
|
||||||
|
} else if width, ok := border.properties[Width]; ok {
|
||||||
|
result.Set(Width, width)
|
||||||
|
}
|
||||||
|
if color, ok := border.properties[tag+"-"+ColorProperty]; ok {
|
||||||
|
result.Set(ColorProperty, color)
|
||||||
|
} else if color, ok := border.properties[ColorProperty]; ok {
|
||||||
|
result.Set(ColorProperty, color)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
case LeftStyle, RightStyle, TopStyle, BottomStyle:
|
||||||
|
if style, ok := border.properties[tag]; ok {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
return border.properties[Style]
|
||||||
|
|
||||||
|
case LeftWidth, RightWidth, TopWidth, BottomWidth:
|
||||||
|
if width, ok := border.properties[tag]; ok {
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
return border.properties[Width]
|
||||||
|
|
||||||
|
case LeftColor, RightColor, TopColor, BottomColor:
|
||||||
|
if color, ok := border.properties[tag]; ok {
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
return border.properties[ColorProperty]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) delete(tag string) {
|
||||||
|
tag = border.normalizeTag(tag)
|
||||||
|
remove := []string{}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Style:
|
||||||
|
remove = []string{Style, LeftStyle, RightStyle, TopStyle, BottomStyle}
|
||||||
|
|
||||||
|
case Width:
|
||||||
|
remove = []string{Width, LeftWidth, RightWidth, TopWidth, BottomWidth}
|
||||||
|
|
||||||
|
case ColorProperty:
|
||||||
|
remove = []string{ColorProperty, LeftColor, RightColor, TopColor, BottomColor}
|
||||||
|
|
||||||
|
case Left, Right, Top, Bottom:
|
||||||
|
if border.Get(Style) != nil {
|
||||||
|
border.properties[tag+"-"+Style] = 0
|
||||||
|
remove = []string{tag + "-" + ColorProperty, tag + "-" + Width}
|
||||||
|
} else {
|
||||||
|
remove = []string{tag + "-" + Style, tag + "-" + ColorProperty, tag + "-" + Width}
|
||||||
|
}
|
||||||
|
|
||||||
|
case LeftStyle, RightStyle, TopStyle, BottomStyle:
|
||||||
|
if border.Get(Style) != nil {
|
||||||
|
border.properties[tag] = 0
|
||||||
|
} else {
|
||||||
|
remove = []string{tag}
|
||||||
|
}
|
||||||
|
|
||||||
|
case LeftWidth, RightWidth, TopWidth, BottomWidth:
|
||||||
|
if border.Get(Width) != nil {
|
||||||
|
border.properties[tag] = AutoSize()
|
||||||
|
} else {
|
||||||
|
remove = []string{tag}
|
||||||
|
}
|
||||||
|
|
||||||
|
case LeftColor, RightColor, TopColor, BottomColor:
|
||||||
|
if border.Get(ColorProperty) != nil {
|
||||||
|
border.properties[tag] = 0
|
||||||
|
} else {
|
||||||
|
remove = []string{tag}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range remove {
|
||||||
|
delete(border.properties, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) ViewBorders(session Session) ViewBorders {
|
||||||
|
|
||||||
|
defStyle, _ := valueToEnum(border.getRaw(Style), BorderStyle, session, NoneLine)
|
||||||
|
defWidth, _ := sizeProperty(border, Width, session)
|
||||||
|
defColor, _ := colorProperty(border, ColorProperty, session)
|
||||||
|
|
||||||
|
getBorder := func(prefix string) ViewBorder {
|
||||||
|
var result ViewBorder
|
||||||
|
var ok bool
|
||||||
|
if result.Style, ok = valueToEnum(border.getRaw(prefix+Style), BorderStyle, session, NoneLine); !ok {
|
||||||
|
result.Style = defStyle
|
||||||
|
}
|
||||||
|
if result.Width, ok = sizeProperty(border, prefix+Width, session); !ok {
|
||||||
|
result.Width = defWidth
|
||||||
|
}
|
||||||
|
if result.Color, ok = colorProperty(border, prefix+ColorProperty, session); !ok {
|
||||||
|
result.Color = defColor
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return ViewBorders{
|
||||||
|
Top: getBorder("top-"),
|
||||||
|
Left: getBorder("left-"),
|
||||||
|
Right: getBorder("right-"),
|
||||||
|
Bottom: getBorder("bottom-"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) cssStyle(builder cssBuilder, session Session) {
|
||||||
|
borders := border.ViewBorders(session)
|
||||||
|
values := enumProperties[BorderStyle].cssValues
|
||||||
|
if borders.Top.Style == borders.Right.Style &&
|
||||||
|
borders.Top.Style == borders.Left.Style &&
|
||||||
|
borders.Top.Style == borders.Bottom.Style {
|
||||||
|
builder.add(BorderStyle, values[borders.Top.Style])
|
||||||
|
} else {
|
||||||
|
builder.addValues(BorderStyle, " ", values[borders.Top.Style],
|
||||||
|
values[borders.Right.Style], values[borders.Bottom.Style], values[borders.Left.Style])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) cssWidth(builder cssBuilder, session Session) {
|
||||||
|
borders := border.ViewBorders(session)
|
||||||
|
if borders.Top.Width == borders.Right.Width &&
|
||||||
|
borders.Top.Width == borders.Left.Width &&
|
||||||
|
borders.Top.Width == borders.Bottom.Width {
|
||||||
|
if borders.Top.Width.Type != Auto {
|
||||||
|
builder.add("border-width", borders.Top.Width.cssString("0"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.addValues("border-width", " ", borders.Top.Width.cssString("0"),
|
||||||
|
borders.Right.Width.cssString("0"), borders.Bottom.Width.cssString("0"), borders.Left.Width.cssString("0"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) cssColor(builder cssBuilder, session Session) {
|
||||||
|
borders := border.ViewBorders(session)
|
||||||
|
if borders.Top.Color == borders.Right.Color &&
|
||||||
|
borders.Top.Color == borders.Left.Color &&
|
||||||
|
borders.Top.Color == borders.Bottom.Color {
|
||||||
|
if borders.Top.Color != 0 {
|
||||||
|
builder.add("border-color", borders.Top.Color.cssString())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.addValues("border-color", " ", borders.Top.Color.cssString(),
|
||||||
|
borders.Right.Color.cssString(), borders.Bottom.Color.cssString(), borders.Left.Color.cssString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) cssStyleValue(session Session) string {
|
||||||
|
var builder cssValueBuilder
|
||||||
|
border.cssStyle(&builder, session)
|
||||||
|
return builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) cssWidthValue(session Session) string {
|
||||||
|
var builder cssValueBuilder
|
||||||
|
border.cssWidth(&builder, session)
|
||||||
|
return builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (border *borderProperty) cssColorValue(session Session) string {
|
||||||
|
var builder cssValueBuilder
|
||||||
|
border.cssColor(&builder, session)
|
||||||
|
return builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewBorder describes parameters of a view border
|
||||||
|
type ViewBorder struct {
|
||||||
|
Style int
|
||||||
|
Color Color
|
||||||
|
Width SizeUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewBorders describes the top, right, bottom, and left border of a view
|
||||||
|
type ViewBorders struct {
|
||||||
|
Top, Right, Bottom, Left ViewBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllTheSame returns true if all borders are the same
|
||||||
|
func (border *ViewBorders) AllTheSame() bool {
|
||||||
|
return border.Top.Style == border.Right.Style &&
|
||||||
|
border.Top.Style == border.Left.Style &&
|
||||||
|
border.Top.Style == border.Bottom.Style &&
|
||||||
|
border.Top.Color == border.Right.Color &&
|
||||||
|
border.Top.Color == border.Left.Color &&
|
||||||
|
border.Top.Color == border.Bottom.Color &&
|
||||||
|
border.Top.Width.Equal(border.Right.Width) &&
|
||||||
|
border.Top.Width.Equal(border.Left.Width) &&
|
||||||
|
border.Top.Width.Equal(border.Bottom.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBorder(style Properties, tag string) BorderProperty {
|
||||||
|
if value := style.Get(tag); value != nil {
|
||||||
|
if border, ok := value.(BorderProperty); ok {
|
||||||
|
return border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,405 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BorderProperty is the interface of a bounds property data
|
||||||
|
type BoundsProperty interface {
|
||||||
|
Properties
|
||||||
|
ruiStringer
|
||||||
|
fmt.Stringer
|
||||||
|
Bounds(session Session) Bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
type boundsPropertyData struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBoundsProperty creates the new BoundsProperty object
|
||||||
|
func NewBoundsProperty(params Params) BoundsProperty {
|
||||||
|
bounds := new(boundsPropertyData)
|
||||||
|
bounds.properties = map[string]interface{}{}
|
||||||
|
if params != nil {
|
||||||
|
for _, tag := range []string{Top, Right, Bottom, Left} {
|
||||||
|
if value, ok := params[tag]; ok {
|
||||||
|
bounds.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *boundsPropertyData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case MarginTop, PaddingTop, CellPaddingTop,
|
||||||
|
"top-margin", "top-padding", "top-cell-padding":
|
||||||
|
tag = Top
|
||||||
|
|
||||||
|
case MarginRight, PaddingRight, CellPaddingRight,
|
||||||
|
"right-margin", "right-padding", "right-cell-padding":
|
||||||
|
tag = Right
|
||||||
|
|
||||||
|
case MarginBottom, PaddingBottom, CellPaddingBottom,
|
||||||
|
"bottom-margin", "bottom-padding", "bottom-cell-padding":
|
||||||
|
tag = Bottom
|
||||||
|
|
||||||
|
case MarginLeft, PaddingLeft, CellPaddingLeft,
|
||||||
|
"left-margin", "left-padding", "left-cell-padding":
|
||||||
|
tag = Left
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *boundsPropertyData) ruiString(writer ruiWriter) {
|
||||||
|
writer.startObject("_")
|
||||||
|
|
||||||
|
for _, tag := range []string{Top, Right, Bottom, Left} {
|
||||||
|
if value, ok := bounds.properties[tag]; ok {
|
||||||
|
writer.writeProperty(Style, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *boundsPropertyData) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
bounds.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *boundsPropertyData) Remove(tag string) {
|
||||||
|
bounds.propertyList.Remove(bounds.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *boundsPropertyData) Set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
bounds.Remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = bounds.normalizeTag(tag)
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Top, Right, Bottom, Left:
|
||||||
|
return bounds.setSizeProperty(tag, value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF(`"%s" property is not compatible with the BoundsProperty`, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *boundsPropertyData) Get(tag string) interface{} {
|
||||||
|
tag = bounds.normalizeTag(tag)
|
||||||
|
if value, ok := bounds.properties[tag]; ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *boundsPropertyData) Bounds(session Session) Bounds {
|
||||||
|
top, _ := sizeProperty(bounds, Top, session)
|
||||||
|
right, _ := sizeProperty(bounds, Right, session)
|
||||||
|
bottom, _ := sizeProperty(bounds, Bottom, session)
|
||||||
|
left, _ := sizeProperty(bounds, Left, session)
|
||||||
|
return Bounds{Top: top, Right: right, Bottom: bottom, Left: left}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds describe bounds of rectangle.
|
||||||
|
type Bounds struct {
|
||||||
|
Top, Right, Bottom, Left SizeUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultBounds return bounds with Top, Right, Bottom and Left fields set to Auto
|
||||||
|
func DefaultBounds() Bounds {
|
||||||
|
return Bounds{
|
||||||
|
Top: SizeUnit{Type: Auto, Value: 0},
|
||||||
|
Right: SizeUnit{Type: Auto, Value: 0},
|
||||||
|
Bottom: SizeUnit{Type: Auto, Value: 0},
|
||||||
|
Left: SizeUnit{Type: Auto, Value: 0},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAll set the Top, Right, Bottom and Left field to the equal value
|
||||||
|
func (bounds *Bounds) SetAll(value SizeUnit) {
|
||||||
|
bounds.Top = value
|
||||||
|
bounds.Right = value
|
||||||
|
bounds.Bottom = value
|
||||||
|
bounds.Left = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *Bounds) parse(value string, session Session) bool {
|
||||||
|
var ok bool
|
||||||
|
if value, ok = session.resolveConstants(value); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
values := strings.Split(value, ",")
|
||||||
|
switch len(values) {
|
||||||
|
case 1:
|
||||||
|
if bounds.Left, ok = StringToSizeUnit(values[0]); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
bounds.Right.Type = bounds.Left.Type
|
||||||
|
bounds.Right.Value = bounds.Left.Value
|
||||||
|
bounds.Top.Type = bounds.Left.Type
|
||||||
|
bounds.Top.Value = bounds.Left.Value
|
||||||
|
bounds.Bottom.Type = bounds.Left.Type
|
||||||
|
bounds.Bottom.Value = bounds.Left.Value
|
||||||
|
return true
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
if values[4] != "" {
|
||||||
|
ErrorLog("invalid Bounds value '" + value + "' (needs 1 or 4 elements separeted by comma)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
if bounds.Top, ok = StringToSizeUnit(values[0]); ok {
|
||||||
|
if bounds.Right, ok = StringToSizeUnit(values[1]); ok {
|
||||||
|
if bounds.Bottom, ok = StringToSizeUnit(values[2]); ok {
|
||||||
|
if bounds.Left, ok = StringToSizeUnit(values[3]); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog("invalid Bounds value '" + value + "' (needs 1 or 4 elements separeted by comma)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *Bounds) setFromProperties(tag, topTag, rightTag, bottomTag, leftTag string, properties Properties, session Session) {
|
||||||
|
bounds.Top = AutoSize()
|
||||||
|
if size, ok := sizeProperty(properties, tag, session); ok {
|
||||||
|
bounds.Top = size
|
||||||
|
}
|
||||||
|
bounds.Right = bounds.Top
|
||||||
|
bounds.Bottom = bounds.Top
|
||||||
|
bounds.Left = bounds.Top
|
||||||
|
|
||||||
|
if size, ok := sizeProperty(properties, topTag, session); ok {
|
||||||
|
bounds.Top = size
|
||||||
|
}
|
||||||
|
if size, ok := sizeProperty(properties, rightTag, session); ok {
|
||||||
|
bounds.Right = size
|
||||||
|
}
|
||||||
|
if size, ok := sizeProperty(properties, bottomTag, session); ok {
|
||||||
|
bounds.Bottom = size
|
||||||
|
}
|
||||||
|
if size, ok := sizeProperty(properties, leftTag, session); ok {
|
||||||
|
bounds.Left = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *Bounds) allFieldsAuto() bool {
|
||||||
|
return bounds.Left.Type == Auto &&
|
||||||
|
bounds.Top.Type == Auto &&
|
||||||
|
bounds.Right.Type == Auto &&
|
||||||
|
bounds.Bottom.Type == Auto
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (bounds *Bounds) allFieldsZero() bool {
|
||||||
|
return (bounds.Left.Type == Auto || bounds.Left.Value == 0) &&
|
||||||
|
(bounds.Top.Type == Auto || bounds.Top.Value == 0) &&
|
||||||
|
(bounds.Right.Type == Auto || bounds.Right.Value == 0) &&
|
||||||
|
(bounds.Bottom.Type == Auto || bounds.Bottom.Value == 0)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (bounds *Bounds) allFieldsEqual() bool {
|
||||||
|
if bounds.Left.Type == bounds.Top.Type &&
|
||||||
|
bounds.Left.Type == bounds.Right.Type &&
|
||||||
|
bounds.Left.Type == bounds.Bottom.Type {
|
||||||
|
return bounds.Left.Type == Auto ||
|
||||||
|
(bounds.Left.Value == bounds.Top.Value &&
|
||||||
|
bounds.Left.Value == bounds.Right.Value &&
|
||||||
|
bounds.Left.Value == bounds.Bottom.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds Bounds) writeCSSString(buffer *strings.Builder, textForAuto string) {
|
||||||
|
buffer.WriteString(bounds.Top.cssString(textForAuto))
|
||||||
|
if !bounds.allFieldsEqual() {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(bounds.Right.cssString(textForAuto))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(bounds.Bottom.cssString(textForAuto))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(bounds.Left.cssString(textForAuto))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String convert Bounds to string
|
||||||
|
func (bounds *Bounds) String() string {
|
||||||
|
if bounds.allFieldsEqual() {
|
||||||
|
return bounds.Top.String()
|
||||||
|
}
|
||||||
|
return bounds.Top.String() + "," + bounds.Right.String() + "," +
|
||||||
|
bounds.Bottom.String() + "," + bounds.Left.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *Bounds) cssValue(tag string, builder cssBuilder) {
|
||||||
|
if bounds.allFieldsEqual() {
|
||||||
|
builder.add(tag, bounds.Top.cssString("0"))
|
||||||
|
} else {
|
||||||
|
builder.addValues(tag, " ", bounds.Top.cssString("0"), bounds.Right.cssString("0"),
|
||||||
|
bounds.Bottom.cssString("0"), bounds.Left.cssString("0"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bounds *Bounds) cssString() string {
|
||||||
|
var builder cssValueBuilder
|
||||||
|
bounds.cssValue("", &builder)
|
||||||
|
return builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setBounds(tag string, value interface{}) bool {
|
||||||
|
if !properties.setSimpleProperty(tag, value) {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if strings.Contains(value, ",") {
|
||||||
|
values := split4Values(value)
|
||||||
|
count := len(values)
|
||||||
|
switch count {
|
||||||
|
case 1:
|
||||||
|
value = values[0]
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
bounds := NewBoundsProperty(nil)
|
||||||
|
for i, tag := range []string{Top, Right, Bottom, Left} {
|
||||||
|
if !bounds.Set(tag, values[i]) {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties.properties[tag] = bounds
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return properties.setSizeProperty(tag, value)
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
properties.properties[tag] = value
|
||||||
|
|
||||||
|
case Bounds:
|
||||||
|
properties.properties[tag] = value
|
||||||
|
|
||||||
|
case BoundsProperty:
|
||||||
|
properties.properties[tag] = value
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
bounds := NewBoundsProperty(nil)
|
||||||
|
for _, tag := range []string{Top, Right, Bottom, Left} {
|
||||||
|
if text, ok := value.PropertyValue(tag); ok {
|
||||||
|
if !bounds.Set(tag, text) {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties.properties[tag] = bounds
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) boundsProperty(tag string) BoundsProperty {
|
||||||
|
if value, ok := properties.properties[tag]; ok {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
bounds := NewBoundsProperty(nil)
|
||||||
|
for _, t := range []string{Top, Right, Bottom, Left} {
|
||||||
|
bounds.Set(t, value)
|
||||||
|
}
|
||||||
|
return bounds
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
bounds := NewBoundsProperty(nil)
|
||||||
|
for _, t := range []string{Top, Right, Bottom, Left} {
|
||||||
|
bounds.Set(t, value)
|
||||||
|
}
|
||||||
|
return bounds
|
||||||
|
|
||||||
|
case BoundsProperty:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case Bounds:
|
||||||
|
return NewBoundsProperty(Params{
|
||||||
|
Top: value.Top,
|
||||||
|
Right: value.Right,
|
||||||
|
Bottom: value.Bottom,
|
||||||
|
Left: value.Left})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewBoundsProperty(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) removeBoundsSide(mainTag, sideTag string) {
|
||||||
|
bounds := properties.boundsProperty(mainTag)
|
||||||
|
if bounds.Get(sideTag) != nil {
|
||||||
|
bounds.Remove(sideTag)
|
||||||
|
properties.properties[mainTag] = bounds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setBoundsSide(mainTag, sideTag string, value interface{}) bool {
|
||||||
|
bounds := properties.boundsProperty(mainTag)
|
||||||
|
if bounds.Set(sideTag, value) {
|
||||||
|
properties.properties[mainTag] = bounds
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
notCompatibleType(sideTag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func boundsProperty(properties Properties, tag string, session Session) (Bounds, bool) {
|
||||||
|
if value := properties.Get(tag); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
if size, ok := StringToSizeUnit(text); ok {
|
||||||
|
return Bounds{Left: size, Top: size, Right: size, Bottom: size}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
return Bounds{Left: value, Top: value, Right: value, Bottom: value}, true
|
||||||
|
|
||||||
|
case Bounds:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case BoundsProperty:
|
||||||
|
return value.Bounds(session), true
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultBounds(), false
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
/*
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBoundsSet(t *testing.T) {
|
||||||
|
|
||||||
|
session := createTestSession(t)
|
||||||
|
|
||||||
|
obj := NewDataObject("Test")
|
||||||
|
obj.SetPropertyValue("x", "10")
|
||||||
|
obj.SetPropertyValue("padding", "8px")
|
||||||
|
obj.SetPropertyValue("margins", "16mm,10pt,12in,auto")
|
||||||
|
obj.SetPropertyValue("fail1", "x16mm")
|
||||||
|
obj.SetPropertyValue("fail2", "16mm,10pt,12in")
|
||||||
|
obj.SetPropertyValue("fail3", "x16mm,10pt,12in,auto")
|
||||||
|
obj.SetPropertyValue("fail4", "16mm,x10pt,12in,auto")
|
||||||
|
obj.SetPropertyValue("fail5", "16mm,10pt,x12in,auto")
|
||||||
|
obj.SetPropertyValue("fail6", "16mm,10pt,12in,autoo")
|
||||||
|
|
||||||
|
const failAttrsCount = 6
|
||||||
|
|
||||||
|
var bounds Bounds
|
||||||
|
if bounds.setProperty(obj, "padding", session) {
|
||||||
|
if bounds.Left.Type != SizeInPixel || bounds.Left.Value != 8 ||
|
||||||
|
bounds.Left != bounds.Right ||
|
||||||
|
bounds.Left != bounds.Top ||
|
||||||
|
bounds.Left != bounds.Bottom {
|
||||||
|
t.Errorf("set padding error, result %v", bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bounds.setProperty(obj, "margins", session) {
|
||||||
|
if bounds.Top.Type != SizeInMM || bounds.Top.Value != 16 ||
|
||||||
|
bounds.Right.Type != SizeInPt || bounds.Right.Value != 10 ||
|
||||||
|
bounds.Bottom.Type != SizeInInch || bounds.Bottom.Value != 12 ||
|
||||||
|
bounds.Left.Type != Auto {
|
||||||
|
t.Errorf("set margins error, result %v", bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreTestLog = true
|
||||||
|
for i := 1; i <= failAttrsCount; i++ {
|
||||||
|
if bounds.setProperty(obj, "fail"+strconv.Itoa(i), session) {
|
||||||
|
t.Errorf("set 'fail' error, result %v", bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ignoreTestLog = false
|
||||||
|
|
||||||
|
obj.SetPropertyValue("padding-left", "10mm")
|
||||||
|
obj.SetPropertyValue("padding-top", "4pt")
|
||||||
|
obj.SetPropertyValue("padding-right", "12in")
|
||||||
|
obj.SetPropertyValue("padding-bottom", "8px")
|
||||||
|
|
||||||
|
if bounds.setProperty(obj, "padding", session) {
|
||||||
|
if bounds.Left.Type != SizeInMM || bounds.Left.Value != 10 ||
|
||||||
|
bounds.Top.Type != SizeInPt || bounds.Top.Value != 4 ||
|
||||||
|
bounds.Right.Type != SizeInInch || bounds.Right.Value != 12 ||
|
||||||
|
bounds.Bottom.Type != SizeInPixel || bounds.Bottom.Value != 8 {
|
||||||
|
t.Errorf("set margins error, result %v", bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range []string{"padding-left", "padding-top", "padding-right", "padding-bottom"} {
|
||||||
|
if old, ok := obj.PropertyValue(tag); ok {
|
||||||
|
ignoreTestLog = true
|
||||||
|
obj.SetPropertyValue(tag, "x")
|
||||||
|
if bounds.setProperty(obj, "padding", session) {
|
||||||
|
t.Errorf("set \"%s\" value \"x\": result %v ", tag, bounds)
|
||||||
|
}
|
||||||
|
ignoreTestLog = false
|
||||||
|
obj.SetPropertyValue(tag, old)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoundsWriteData(t *testing.T) {
|
||||||
|
|
||||||
|
_ = createTestSession(t)
|
||||||
|
|
||||||
|
bounds := Bounds{
|
||||||
|
SizeUnit{SizeInPixel, 8},
|
||||||
|
SizeUnit{SizeInInch, 10},
|
||||||
|
SizeUnit{SizeInPt, 12},
|
||||||
|
SizeUnit{Auto, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
bounds.writeData(buffer)
|
||||||
|
str := buffer.String()
|
||||||
|
if str != `"8px,10in,12pt,auto"` {
|
||||||
|
t.Errorf("result `%s`, expected `\"8px,10dip,12pt,auto\"`", str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,36 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
// Button - button view
|
||||||
|
type Button interface {
|
||||||
|
CustomView
|
||||||
|
}
|
||||||
|
|
||||||
|
type buttonData struct {
|
||||||
|
CustomViewData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewButton create new Button object and return it
|
||||||
|
func NewButton(session Session, params Params) Button {
|
||||||
|
button := new(buttonData)
|
||||||
|
InitCustomView(button, "Button", session, params)
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
|
||||||
|
func newButton(session Session) View {
|
||||||
|
return NewButton(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *buttonData) CreateSuperView(session Session) View {
|
||||||
|
return NewListLayout(session, Params{
|
||||||
|
Semantics: ButtonSemantics,
|
||||||
|
Style: "ruiButton",
|
||||||
|
StyleDisabled: "ruiDisabledButton",
|
||||||
|
HorizontalAlign: CenterAlign,
|
||||||
|
VerticalAlign: CenterAlign,
|
||||||
|
Orientation: StartToEndOrientation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *buttonData) Focusable() bool {
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// DrawFunction is the constant for the "draw-function" property tag.
|
||||||
|
// The "draw-function" property sets the draw function of CanvasView.
|
||||||
|
// The function should have the following format: func(Canvas)
|
||||||
|
const DrawFunction = "draw-function"
|
||||||
|
|
||||||
|
// CanvasView interface of a custom draw view
|
||||||
|
type CanvasView interface {
|
||||||
|
View
|
||||||
|
Redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
type canvasViewData struct {
|
||||||
|
viewData
|
||||||
|
drawer func(Canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCanvasView creates the new custom draw view
|
||||||
|
func NewCanvasView(session Session, params Params) CanvasView {
|
||||||
|
view := new(canvasViewData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCanvasView(session Session) View {
|
||||||
|
return NewCanvasView(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of ViewsContainer by default values
|
||||||
|
func (canvasView *canvasViewData) Init(session Session) {
|
||||||
|
canvasView.viewData.Init(session)
|
||||||
|
canvasView.tag = "CanvasView"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case "draw-func":
|
||||||
|
tag = DrawFunction
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) Remove(tag string) {
|
||||||
|
canvasView.remove(canvasView.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) remove(tag string) {
|
||||||
|
if tag == DrawFunction {
|
||||||
|
canvasView.drawer = nil
|
||||||
|
canvasView.Redraw()
|
||||||
|
} else {
|
||||||
|
canvasView.viewData.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) Set(tag string, value interface{}) bool {
|
||||||
|
return canvasView.set(canvasView.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) set(tag string, value interface{}) bool {
|
||||||
|
if tag == DrawFunction {
|
||||||
|
if value == nil {
|
||||||
|
canvasView.drawer = nil
|
||||||
|
} else if fn, ok := value.(func(Canvas)); ok {
|
||||||
|
canvasView.drawer = fn
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
canvasView.Redraw()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return canvasView.viewData.set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) Get(tag string) interface{} {
|
||||||
|
return canvasView.get(canvasView.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) get(tag string) interface{} {
|
||||||
|
if tag == DrawFunction {
|
||||||
|
return canvasView.drawer
|
||||||
|
}
|
||||||
|
return canvasView.viewData.get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) htmlTag() string {
|
||||||
|
return "canvas"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) Redraw() {
|
||||||
|
if canvasView.drawer != nil {
|
||||||
|
canvas := newCanvas(canvasView)
|
||||||
|
canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height)
|
||||||
|
if canvasView.drawer != nil {
|
||||||
|
canvasView.drawer(canvas)
|
||||||
|
}
|
||||||
|
canvasView.session.runScript(canvas.finishDraw())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (canvasView *canvasViewData) onResize(self View, x, y, width, height float64) {
|
||||||
|
canvasView.viewData.onResize(self, x, y, width, height)
|
||||||
|
canvasView.Redraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedrawCanvasView finds CanvasView with canvasViewID and redraws it
|
||||||
|
func RedrawCanvasView(rootView View, canvasViewID string) {
|
||||||
|
if canvas := CanvasViewByID(rootView, canvasViewID); canvas != nil {
|
||||||
|
canvas.Redraw()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,374 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckboxChangedEvent is the constant for "checkbox-event" property tag.
|
||||||
|
// The "checkbox-event" event occurs when the checkbox becomes checked/unchecked.
|
||||||
|
// The main listener format: func(Checkbox, bool), where the second argument is the checkbox state.
|
||||||
|
const CheckboxChangedEvent = "checkbox-event"
|
||||||
|
|
||||||
|
// Checkbox - checkbox view
|
||||||
|
type Checkbox interface {
|
||||||
|
ViewsContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type checkboxData struct {
|
||||||
|
viewsContainerData
|
||||||
|
checkedListeners []func(Checkbox, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCheckbox create new Checkbox object and return it
|
||||||
|
func NewCheckbox(session Session, params Params) Checkbox {
|
||||||
|
view := new(checkboxData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, Params{
|
||||||
|
ClickEvent: checkboxClickListener,
|
||||||
|
KeyDownEvent: checkboxKeyListener,
|
||||||
|
})
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCheckbox(session Session) View {
|
||||||
|
return NewCheckbox(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) Init(session Session) {
|
||||||
|
button.viewsContainerData.Init(session)
|
||||||
|
button.tag = "Checkbox"
|
||||||
|
button.systemClass = "ruiGridLayout ruiCheckbox"
|
||||||
|
button.checkedListeners = []func(Checkbox, bool){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) Focusable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) Get(tag string) interface{} {
|
||||||
|
switch strings.ToLower(tag) {
|
||||||
|
case CheckboxChangedEvent:
|
||||||
|
return button.checkedListeners
|
||||||
|
}
|
||||||
|
|
||||||
|
return button.viewsContainerData.Get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) Set(tag string, value interface{}) bool {
|
||||||
|
switch strings.ToLower(tag) {
|
||||||
|
case CheckboxChangedEvent:
|
||||||
|
ok := button.setChangedListener(value)
|
||||||
|
if !ok {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
|
||||||
|
case Checked:
|
||||||
|
oldChecked := button.checked()
|
||||||
|
if !button.setBoolProperty(Checked, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if button.created {
|
||||||
|
checked := button.checked()
|
||||||
|
if checked != oldChecked {
|
||||||
|
button.changedCheckboxState(checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
|
||||||
|
if button.setEnumProperty(tag, value, enumProperties[tag].values) {
|
||||||
|
if button.created {
|
||||||
|
htmlID := button.htmlID()
|
||||||
|
updateCSSStyle(htmlID, button.session)
|
||||||
|
updateInnerHTML(htmlID, button.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case VerticalAlign:
|
||||||
|
if button.setEnumProperty(tag, value, enumProperties[tag].values) {
|
||||||
|
if button.created {
|
||||||
|
updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case HorizontalAlign:
|
||||||
|
if button.setEnumProperty(tag, value, enumProperties[tag].values) {
|
||||||
|
if button.created {
|
||||||
|
updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return button.viewsContainerData.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) Remove(tag string) {
|
||||||
|
switch strings.ToLower(tag) {
|
||||||
|
case CheckboxChangedEvent:
|
||||||
|
if len(button.checkedListeners) > 0 {
|
||||||
|
button.checkedListeners = []func(Checkbox, bool){}
|
||||||
|
}
|
||||||
|
|
||||||
|
case Checked:
|
||||||
|
oldChecked := button.checked()
|
||||||
|
delete(button.properties, tag)
|
||||||
|
if oldChecked {
|
||||||
|
button.changedCheckboxState(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
|
||||||
|
delete(button.properties, tag)
|
||||||
|
htmlID := button.htmlID()
|
||||||
|
updateCSSStyle(htmlID, button.session)
|
||||||
|
updateInnerHTML(htmlID, button.session)
|
||||||
|
|
||||||
|
case VerticalAlign:
|
||||||
|
delete(button.properties, tag)
|
||||||
|
updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session)
|
||||||
|
|
||||||
|
case HorizontalAlign:
|
||||||
|
delete(button.properties, tag)
|
||||||
|
updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session)
|
||||||
|
|
||||||
|
default:
|
||||||
|
button.viewsContainerData.Remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) checked() bool {
|
||||||
|
checked, _ := boolProperty(button, Checked, button.Session())
|
||||||
|
return checked
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) changedCheckboxState(state bool) {
|
||||||
|
for _, listener := range button.checkedListeners {
|
||||||
|
listener(button, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
button.htmlCheckbox(buffer, state)
|
||||||
|
button.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, button.htmlID()+"checkbox", buffer.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkboxClickListener(view View) {
|
||||||
|
view.Set(Checked, !IsCheckboxChecked(view, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkboxKeyListener(view View, event KeyEvent) {
|
||||||
|
switch event.Code {
|
||||||
|
case "Enter", "Space":
|
||||||
|
view.Set(Checked, !IsCheckboxChecked(view, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) setChangedListener(value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
if len(button.checkedListeners) > 0 {
|
||||||
|
button.checkedListeners = []func(Checkbox, bool){}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(Checkbox, bool):
|
||||||
|
button.checkedListeners = []func(Checkbox, bool){value}
|
||||||
|
|
||||||
|
case func(bool):
|
||||||
|
fn := func(view Checkbox, checked bool) {
|
||||||
|
value(checked)
|
||||||
|
}
|
||||||
|
button.checkedListeners = []func(Checkbox, bool){fn}
|
||||||
|
|
||||||
|
case []func(Checkbox, bool):
|
||||||
|
button.checkedListeners = value
|
||||||
|
|
||||||
|
case []func(bool):
|
||||||
|
listeners := make([]func(Checkbox, bool), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[i] = func(view Checkbox, checked bool) {
|
||||||
|
val(checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.checkedListeners = listeners
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
listeners := make([]func(Checkbox, bool), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := val.(type) {
|
||||||
|
case func(Checkbox, bool):
|
||||||
|
listeners[i] = val
|
||||||
|
|
||||||
|
case func(bool):
|
||||||
|
listeners[i] = func(view Checkbox, date bool) {
|
||||||
|
val(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button.checkedListeners = listeners
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) cssStyle(self View, builder cssBuilder) {
|
||||||
|
session := button.Session()
|
||||||
|
vAlign, _ := enumStyledProperty(button, CheckboxVerticalAlign, LeftAlign)
|
||||||
|
hAlign, _ := enumStyledProperty(button, CheckboxHorizontalAlign, TopAlign)
|
||||||
|
switch hAlign {
|
||||||
|
case CenterAlign:
|
||||||
|
if vAlign == BottomAlign {
|
||||||
|
builder.add("grid-template-rows", "1fr auto")
|
||||||
|
} else {
|
||||||
|
builder.add("grid-template-rows", "auto 1fr")
|
||||||
|
}
|
||||||
|
|
||||||
|
case RightAlign:
|
||||||
|
builder.add("grid-template-columns", "1fr auto")
|
||||||
|
|
||||||
|
default:
|
||||||
|
builder.add("grid-template-columns", "auto 1fr")
|
||||||
|
}
|
||||||
|
|
||||||
|
if gap, ok := sizeConstant(session, "ruiCheckboxGap"); ok && gap.Type != Auto && gap.Value > 0 {
|
||||||
|
builder.add("gap", gap.cssString("0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add("align-items", "stretch")
|
||||||
|
builder.add("justify-items", "stretch")
|
||||||
|
|
||||||
|
button.viewsContainerData.cssStyle(self, builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool) (int, int) {
|
||||||
|
vAlign, _ := enumStyledProperty(button, CheckboxVerticalAlign, LeftAlign)
|
||||||
|
hAlign, _ := enumStyledProperty(button, CheckboxHorizontalAlign, TopAlign)
|
||||||
|
|
||||||
|
buffer.WriteString(`<div id="`)
|
||||||
|
buffer.WriteString(button.htmlID())
|
||||||
|
buffer.WriteString(`checkbox" style="display: grid;`)
|
||||||
|
if hAlign == CenterAlign {
|
||||||
|
buffer.WriteString(" justify-items: center; grid-column-start: 1; grid-column-end: 2;")
|
||||||
|
if vAlign == BottomAlign {
|
||||||
|
buffer.WriteString(" grid-row-start: 2; grid-row-end: 3;")
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(" grid-row-start: 1; grid-row-end: 2;")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if hAlign == RightAlign {
|
||||||
|
buffer.WriteString(" grid-column-start: 2; grid-column-end: 3;")
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(" grid-column-start: 1; grid-column-end: 2;")
|
||||||
|
}
|
||||||
|
buffer.WriteString(" grid-row-start: 1; grid-row-end: 2;")
|
||||||
|
switch vAlign {
|
||||||
|
case BottomAlign:
|
||||||
|
buffer.WriteString(" align-items: end;")
|
||||||
|
|
||||||
|
case CenterAlign:
|
||||||
|
buffer.WriteString(" align-items: center;")
|
||||||
|
|
||||||
|
default:
|
||||||
|
buffer.WriteString(" align-items: start;")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`">`)
|
||||||
|
if checked {
|
||||||
|
buffer.WriteString(button.Session().checkboxOnImage())
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(button.Session().checkboxOffImage())
|
||||||
|
}
|
||||||
|
buffer.WriteString(`</div>`)
|
||||||
|
|
||||||
|
return vAlign, hAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
|
||||||
|
vCheckboxAlign, hCheckboxAlign := button.htmlCheckbox(buffer, IsCheckboxChecked(button, ""))
|
||||||
|
|
||||||
|
buffer.WriteString(`<div id="`)
|
||||||
|
buffer.WriteString(button.htmlID())
|
||||||
|
buffer.WriteString(`content" style="display: grid;`)
|
||||||
|
if hCheckboxAlign == LeftAlign {
|
||||||
|
buffer.WriteString(" grid-column-start: 2; grid-column-end: 3;")
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(" grid-column-start: 1; grid-column-end: 2;")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hCheckboxAlign == CenterAlign && vCheckboxAlign != BottomAlign {
|
||||||
|
buffer.WriteString(" grid-row-start: 2; grid-row-end: 3;")
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(" grid-row-start: 1; grid-row-end: 2;")
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(" align-items: ")
|
||||||
|
buffer.WriteString(button.cssVerticalAlign())
|
||||||
|
buffer.WriteRune(';')
|
||||||
|
|
||||||
|
buffer.WriteString(" justify-items: ")
|
||||||
|
buffer.WriteString(button.cssHorizontalAlign())
|
||||||
|
buffer.WriteRune(';')
|
||||||
|
|
||||||
|
buffer.WriteString(`">`)
|
||||||
|
button.viewsContainerData.htmlSubviews(self, buffer)
|
||||||
|
buffer.WriteString(`</div>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) cssHorizontalAlign() string {
|
||||||
|
align, _ := enumStyledProperty(button, HorizontalAlign, TopAlign)
|
||||||
|
values := enumProperties[CellHorizontalAlign].cssValues
|
||||||
|
if align >= 0 && align < len(values) {
|
||||||
|
return values[align]
|
||||||
|
}
|
||||||
|
return values[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (button *checkboxData) cssVerticalAlign() string {
|
||||||
|
align, _ := enumStyledProperty(button, VerticalAlign, TopAlign)
|
||||||
|
values := enumProperties[CellVerticalAlign].cssValues
|
||||||
|
if align >= 0 && align < len(values) {
|
||||||
|
return values[align]
|
||||||
|
}
|
||||||
|
return values[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCheckboxChecked returns true if the Checkbox is checked, false otherwise.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func IsCheckboxChecked(view View, subviewID string) bool {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if checked := view.Get(Checked); checked != nil {
|
||||||
|
if b, ok := checked.(bool); ok {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Color - represent color in argb format
|
||||||
|
type Color uint32
|
||||||
|
|
||||||
|
// ARGB - return alpha, red, green and blue components of the color
|
||||||
|
func (color Color) ARGB() (uint8, uint8, uint8, uint8) {
|
||||||
|
return uint8(color >> 24),
|
||||||
|
uint8((color >> 16) & 0xFF),
|
||||||
|
uint8((color >> 8) & 0xFF),
|
||||||
|
uint8(color & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alpha - return the alpha component of the color
|
||||||
|
func (color Color) Alpha() int {
|
||||||
|
return int((color >> 24) & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Red - return the red component of the color
|
||||||
|
func (color Color) Red() int {
|
||||||
|
return int((color >> 16) & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Green - return the green component of the color
|
||||||
|
func (color Color) Green() int {
|
||||||
|
return int((color >> 8) & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blue - return the blue component of the color
|
||||||
|
func (color Color) Blue() int {
|
||||||
|
return int(color & 0xFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String get a text representation of the color
|
||||||
|
func (color Color) String() string {
|
||||||
|
return fmt.Sprintf("#%08X", int(color))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (color Color) rgbString() string {
|
||||||
|
return fmt.Sprintf("#%06X", int(color&0xFFFFFF))
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeData write a text representation of the color to the buffer
|
||||||
|
func (color Color) writeData(buffer *bytes.Buffer) {
|
||||||
|
buffer.WriteString(color.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// cssString get the text representation of the color in CSS format
|
||||||
|
func (color Color) cssString() string {
|
||||||
|
red := color.Red()
|
||||||
|
green := color.Green()
|
||||||
|
blue := color.Blue()
|
||||||
|
|
||||||
|
if alpha := color.Alpha(); alpha < 255 {
|
||||||
|
aText := fmt.Sprintf("%.2f", float64(alpha)/255.0)
|
||||||
|
if len(aText) > 1 {
|
||||||
|
aText = aText[1:]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("rgba(%d,%d,%d,%s)", red, green, blue, aText)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("rgb(%d,%d,%d)", red, green, blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToColor converts the string argument to Color value
|
||||||
|
func StringToColor(text string) (Color, bool) {
|
||||||
|
|
||||||
|
text = strings.Trim(text, " \t\r\n")
|
||||||
|
if text == "" {
|
||||||
|
ErrorLog(`Invalid color value: ""`)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if text[0] == '#' {
|
||||||
|
c, err := strconv.ParseUint(text[1:], 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
ErrorLog("Set color value error: " + err.Error())
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(text) - 1 {
|
||||||
|
case 8:
|
||||||
|
return Color(c), true
|
||||||
|
|
||||||
|
case 6:
|
||||||
|
return Color(c | 0xFF000000), true
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
a := (c >> 12) & 0xF
|
||||||
|
r := (c >> 8) & 0xF
|
||||||
|
g := (c >> 4) & 0xF
|
||||||
|
b := c & 0xF
|
||||||
|
return Color((a << 28) | (a << 24) | (r << 20) | (r << 16) | (g << 12) | (g << 8) | (b << 4) | b), true
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
r := (c >> 8) & 0xF
|
||||||
|
g := (c >> 4) & 0xF
|
||||||
|
b := c & 0xF
|
||||||
|
return Color(0xFF000000 | (r << 20) | (r << 16) | (g << 12) | (g << 8) | (b << 4) | b), true
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog(`Invalid color format: "` + text + `". Valid formats: #AARRGGBB, #RRGGBB, #ARGB, #RGB`)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
parseRGB := func(args string) []int {
|
||||||
|
args = strings.Trim(args, " \t")
|
||||||
|
count := len(args)
|
||||||
|
if count < 3 || args[0] != '(' || args[count-1] != ')' {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := strings.Split(args[1:count-1], ",")
|
||||||
|
result := make([]int, len(arg))
|
||||||
|
for i, val := range arg {
|
||||||
|
val = strings.Trim(val, " \t")
|
||||||
|
size := len(val)
|
||||||
|
if size == 0 {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
if val[size-1] == '%' {
|
||||||
|
if n, err := strconv.Atoi(val[:size-1]); err == nil && n >= 0 && n <= 100 {
|
||||||
|
result[i] = n * 255 / 100
|
||||||
|
} else {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
} else if strings.ContainsRune(val, '.') {
|
||||||
|
if val[0] == '.' {
|
||||||
|
val = "0" + val
|
||||||
|
}
|
||||||
|
if f, err := strconv.ParseFloat(val, 32); err == nil && f >= 0 && f <= 1 {
|
||||||
|
result[i] = int(f * 255)
|
||||||
|
} else {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if n, err := strconv.Atoi(val); err == nil && n >= 0 && n <= 255 {
|
||||||
|
result[i] = n
|
||||||
|
} else {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
text = strings.ToLower(text)
|
||||||
|
if strings.HasPrefix(text, "rgba") {
|
||||||
|
args := parseRGB(text[4:])
|
||||||
|
if len(args) == 4 {
|
||||||
|
return Color((args[3] << 24) | (args[0] << 16) | (args[1] << 8) | args[2]), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(text, "rgb") {
|
||||||
|
args := parseRGB(text[3:])
|
||||||
|
if len(args) == 3 {
|
||||||
|
return Color(0xFF000000 | (args[0] << 16) | (args[1] << 8) | args[2]), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO hsl(360,100%,50%), hsla(360,100%,50%,.5)
|
||||||
|
|
||||||
|
if color, ok := colorConstants[text]; ok {
|
||||||
|
return color, true
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog(`Invalid color format: "` + text + `"`)
|
||||||
|
return 0, false
|
||||||
|
}
|
|
@ -0,0 +1,448 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Black color constant
|
||||||
|
Black Color = 0xff000000
|
||||||
|
// Silver color constant
|
||||||
|
Silver Color = 0xffc0c0c0
|
||||||
|
// Gray color constant
|
||||||
|
Gray Color = 0xff808080
|
||||||
|
// White color constant
|
||||||
|
White Color = 0xffffffff
|
||||||
|
// Maroon color constant
|
||||||
|
Maroon Color = 0xff800000
|
||||||
|
// Red color constant
|
||||||
|
Red Color = 0xffff0000
|
||||||
|
// Purple color constant
|
||||||
|
Purple Color = 0xff800080
|
||||||
|
// Fuchsia color constant
|
||||||
|
Fuchsia Color = 0xffff00ff
|
||||||
|
// Green color constant
|
||||||
|
Green Color = 0xff008000
|
||||||
|
// Lime color constant
|
||||||
|
Lime Color = 0xff00ff00
|
||||||
|
// Olive color constant
|
||||||
|
Olive Color = 0xff808000
|
||||||
|
// Yellow color constant
|
||||||
|
Yellow Color = 0xffffff00
|
||||||
|
// Navy color constant
|
||||||
|
Navy Color = 0xff000080
|
||||||
|
// Blue color constant
|
||||||
|
Blue Color = 0xff0000ff
|
||||||
|
// Teal color constant
|
||||||
|
Teal Color = 0xff008080
|
||||||
|
// Aqua color constant
|
||||||
|
Aqua Color = 0xff00ffff
|
||||||
|
// Orange color constant
|
||||||
|
Orange Color = 0xffffa500
|
||||||
|
// AliceBlue color constant
|
||||||
|
AliceBlue Color = 0xfff0f8ff
|
||||||
|
// AntiqueWhite color constant
|
||||||
|
AntiqueWhite Color = 0xfffaebd7
|
||||||
|
// Aquamarine color constant
|
||||||
|
Aquamarine Color = 0xff7fffd4
|
||||||
|
// Azure color constant
|
||||||
|
Azure Color = 0xfff0ffff
|
||||||
|
// Beige color constant
|
||||||
|
Beige Color = 0xfff5f5dc
|
||||||
|
// Bisque color constant
|
||||||
|
Bisque Color = 0xffffe4c4
|
||||||
|
// BlanchedAlmond color constant
|
||||||
|
BlanchedAlmond Color = 0xffffebcd
|
||||||
|
// BlueViolet color constant
|
||||||
|
BlueViolet Color = 0xff8a2be2
|
||||||
|
// Brown color constant
|
||||||
|
Brown Color = 0xffa52a2a
|
||||||
|
// Burlywood color constant
|
||||||
|
Burlywood Color = 0xffdeb887
|
||||||
|
// CadetBlue color constant
|
||||||
|
CadetBlue Color = 0xff5f9ea0
|
||||||
|
// Chartreuse color constant
|
||||||
|
Chartreuse Color = 0xff7fff00
|
||||||
|
// Chocolate color constant
|
||||||
|
Chocolate Color = 0xffd2691e
|
||||||
|
// Coral color constant
|
||||||
|
Coral Color = 0xffff7f50
|
||||||
|
// CornflowerBlue color constant
|
||||||
|
CornflowerBlue Color = 0xff6495ed
|
||||||
|
// Cornsilk color constant
|
||||||
|
Cornsilk Color = 0xfffff8dc
|
||||||
|
// Crimson color constant
|
||||||
|
Crimson Color = 0xffdc143c
|
||||||
|
// Cyan color constant
|
||||||
|
Cyan Color = 0xff00ffff
|
||||||
|
// DarkBlue color constant
|
||||||
|
DarkBlue Color = 0xff00008b
|
||||||
|
// DarkCyan color constant
|
||||||
|
DarkCyan Color = 0xff008b8b
|
||||||
|
// DarkGoldenRod color constant
|
||||||
|
DarkGoldenRod Color = 0xffb8860b
|
||||||
|
// DarkGray color constant
|
||||||
|
DarkGray Color = 0xffa9a9a9
|
||||||
|
// DarkGreen color constant
|
||||||
|
DarkGreen Color = 0xff006400
|
||||||
|
// DarkGrey color constant
|
||||||
|
DarkGrey Color = 0xffa9a9a9
|
||||||
|
// DarkKhaki color constant
|
||||||
|
DarkKhaki Color = 0xffbdb76b
|
||||||
|
// DarkMagenta color constant
|
||||||
|
DarkMagenta Color = 0xff8b008b
|
||||||
|
// DarkOliveGreen color constant
|
||||||
|
DarkOliveGreen Color = 0xff556b2f
|
||||||
|
// DarkOrange color constant
|
||||||
|
DarkOrange Color = 0xffff8c00
|
||||||
|
// DarkOrchid color constant
|
||||||
|
DarkOrchid Color = 0xff9932cc
|
||||||
|
// DarkRed color constant
|
||||||
|
DarkRed Color = 0xff8b0000
|
||||||
|
// DarkSalmon color constant
|
||||||
|
DarkSalmon Color = 0xffe9967a
|
||||||
|
// DarkSeaGreen color constant
|
||||||
|
DarkSeaGreen Color = 0xff8fbc8f
|
||||||
|
// DarkSlateBlue color constant
|
||||||
|
DarkSlateBlue Color = 0xff483d8b
|
||||||
|
// DarkSlateGray color constant
|
||||||
|
DarkSlateGray Color = 0xff2f4f4f
|
||||||
|
// Darkslategrey color constant
|
||||||
|
Darkslategrey Color = 0xff2f4f4f
|
||||||
|
// DarkTurquoise color constant
|
||||||
|
DarkTurquoise Color = 0xff00ced1
|
||||||
|
// DarkViolet color constant
|
||||||
|
DarkViolet Color = 0xff9400d3
|
||||||
|
// DeepPink color constant
|
||||||
|
DeepPink Color = 0xffff1493
|
||||||
|
// DeepSkyBlue color constant
|
||||||
|
DeepSkyBlue Color = 0xff00bfff
|
||||||
|
// DimGray color constant
|
||||||
|
DimGray Color = 0xff696969
|
||||||
|
// DimGrey color constant
|
||||||
|
DimGrey Color = 0xff696969
|
||||||
|
// DodgerBlue color constant
|
||||||
|
DodgerBlue Color = 0xff1e90ff
|
||||||
|
// FireBrick color constant
|
||||||
|
FireBrick Color = 0xffb22222
|
||||||
|
// FloralWhite color constant
|
||||||
|
FloralWhite Color = 0xfffffaf0
|
||||||
|
// ForestGreen color constant
|
||||||
|
ForestGreen Color = 0xff228b22
|
||||||
|
// Gainsboro color constant
|
||||||
|
Gainsboro Color = 0xffdcdcdc
|
||||||
|
// GhostWhite color constant
|
||||||
|
GhostWhite Color = 0xfff8f8ff
|
||||||
|
// Gold color constant
|
||||||
|
Gold Color = 0xffffd700
|
||||||
|
// GoldenRod color constant
|
||||||
|
GoldenRod Color = 0xffdaa520
|
||||||
|
// GreenyEllow color constant
|
||||||
|
GreenyEllow Color = 0xffadff2f
|
||||||
|
// Grey color constant
|
||||||
|
Grey Color = 0xff808080
|
||||||
|
// Honeydew color constant
|
||||||
|
Honeydew Color = 0xfff0fff0
|
||||||
|
// HotPink color constant
|
||||||
|
HotPink Color = 0xffff69b4
|
||||||
|
// IndianRed color constant
|
||||||
|
IndianRed Color = 0xffcd5c5c
|
||||||
|
// Indigo color constant
|
||||||
|
Indigo Color = 0xff4b0082
|
||||||
|
// Ivory color constant
|
||||||
|
Ivory Color = 0xfffffff0
|
||||||
|
// Khaki color constant
|
||||||
|
Khaki Color = 0xfff0e68c
|
||||||
|
// Lavender color constant
|
||||||
|
Lavender Color = 0xffe6e6fa
|
||||||
|
// LavenderBlush color constant
|
||||||
|
LavenderBlush Color = 0xfffff0f5
|
||||||
|
// LawnGreen color constant
|
||||||
|
LawnGreen Color = 0xff7cfc00
|
||||||
|
// LemonChiffon color constant
|
||||||
|
LemonChiffon Color = 0xfffffacd
|
||||||
|
// LightBlue color constant
|
||||||
|
LightBlue Color = 0xffadd8e6
|
||||||
|
// LightCoral color constant
|
||||||
|
LightCoral Color = 0xfff08080
|
||||||
|
// LightCyan color constant
|
||||||
|
LightCyan Color = 0xffe0ffff
|
||||||
|
// LightGoldenrodYellow color constant
|
||||||
|
LightGoldenrodYellow Color = 0xfffafad2
|
||||||
|
// LightGray color constant
|
||||||
|
LightGray Color = 0xffd3d3d3
|
||||||
|
// LightGreen color constant
|
||||||
|
LightGreen Color = 0xff90ee90
|
||||||
|
// LightGrey color constant
|
||||||
|
LightGrey Color = 0xffd3d3d3
|
||||||
|
// LightPink color constant
|
||||||
|
LightPink Color = 0xffffb6c1
|
||||||
|
// LightSalmon color constant
|
||||||
|
LightSalmon Color = 0xffffa07a
|
||||||
|
// LightSeaGreen color constant
|
||||||
|
LightSeaGreen Color = 0xff20b2aa
|
||||||
|
// LightSkyBlue color constant
|
||||||
|
LightSkyBlue Color = 0xff87cefa
|
||||||
|
// LightSlateGray color constant
|
||||||
|
LightSlateGray Color = 0xff778899
|
||||||
|
// LightSlateGrey color constant
|
||||||
|
LightSlateGrey Color = 0xff778899
|
||||||
|
// LightSteelBlue color constant
|
||||||
|
LightSteelBlue Color = 0xffb0c4de
|
||||||
|
// LightYellow color constant
|
||||||
|
LightYellow Color = 0xffffffe0
|
||||||
|
// LimeGreen color constant
|
||||||
|
LimeGreen Color = 0xff32cd32
|
||||||
|
// Linen color constant
|
||||||
|
Linen Color = 0xfffaf0e6
|
||||||
|
// Magenta color constant
|
||||||
|
Magenta Color = 0xffff00ff
|
||||||
|
// MediumAquamarine color constant
|
||||||
|
MediumAquamarine Color = 0xff66cdaa
|
||||||
|
// MediumBlue color constant
|
||||||
|
MediumBlue Color = 0xff0000cd
|
||||||
|
// MediumOrchid color constant
|
||||||
|
MediumOrchid Color = 0xffba55d3
|
||||||
|
// MediumPurple color constant
|
||||||
|
MediumPurple Color = 0xff9370db
|
||||||
|
// MediumSeaGreen color constant
|
||||||
|
MediumSeaGreen Color = 0xff3cb371
|
||||||
|
// MediumSlateBlue color constant
|
||||||
|
MediumSlateBlue Color = 0xff7b68ee
|
||||||
|
// MediumSpringGreen color constant
|
||||||
|
MediumSpringGreen Color = 0xff00fa9a
|
||||||
|
// MediumTurquoise color constant
|
||||||
|
MediumTurquoise Color = 0xff48d1cc
|
||||||
|
// MediumVioletRed color constant
|
||||||
|
MediumVioletRed Color = 0xffc71585
|
||||||
|
// MidnightBlue color constant
|
||||||
|
MidnightBlue Color = 0xff191970
|
||||||
|
// MintCream color constant
|
||||||
|
MintCream Color = 0xfff5fffa
|
||||||
|
// MistyRose color constant
|
||||||
|
MistyRose Color = 0xffffe4e1
|
||||||
|
// Moccasin color constant
|
||||||
|
Moccasin Color = 0xffffe4b5
|
||||||
|
// NavajoWhite color constant
|
||||||
|
NavajoWhite Color = 0xffffdead
|
||||||
|
// OldLace color constant
|
||||||
|
OldLace Color = 0xfffdf5e6
|
||||||
|
// OliveDrab color constant
|
||||||
|
OliveDrab Color = 0xff6b8e23
|
||||||
|
// OrangeRed color constant
|
||||||
|
OrangeRed Color = 0xffff4500
|
||||||
|
// Orchid color constant
|
||||||
|
Orchid Color = 0xffda70d6
|
||||||
|
// PaleGoldenrod color constant
|
||||||
|
PaleGoldenrod Color = 0xffeee8aa
|
||||||
|
// PaleGreen color constant
|
||||||
|
PaleGreen Color = 0xff98fb98
|
||||||
|
// PaleTurquoise color constant
|
||||||
|
PaleTurquoise Color = 0xffafeeee
|
||||||
|
// PaleVioletRed color constant
|
||||||
|
PaleVioletRed Color = 0xffdb7093
|
||||||
|
// PapayaWhip color constant
|
||||||
|
PapayaWhip Color = 0xffffefd5
|
||||||
|
// PeachPuff color constant
|
||||||
|
PeachPuff Color = 0xffffdab9
|
||||||
|
// Peru color constant
|
||||||
|
Peru Color = 0xffcd853f
|
||||||
|
// Pink color constant
|
||||||
|
Pink Color = 0xffffc0cb
|
||||||
|
// Plum color constant
|
||||||
|
Plum Color = 0xffdda0dd
|
||||||
|
// PowderBlue color constant
|
||||||
|
PowderBlue Color = 0xffb0e0e6
|
||||||
|
// RosyBrown color constant
|
||||||
|
RosyBrown Color = 0xffbc8f8f
|
||||||
|
// RoyalBlue color constant
|
||||||
|
RoyalBlue Color = 0xff4169e1
|
||||||
|
// SaddleBrown color constant
|
||||||
|
SaddleBrown Color = 0xff8b4513
|
||||||
|
// Salmon color constant
|
||||||
|
Salmon Color = 0xfffa8072
|
||||||
|
// SandyBrown color constant
|
||||||
|
SandyBrown Color = 0xfff4a460
|
||||||
|
// SeaGreen color constant
|
||||||
|
SeaGreen Color = 0xff2e8b57
|
||||||
|
// SeaShell color constant
|
||||||
|
SeaShell Color = 0xfffff5ee
|
||||||
|
// Sienna color constant
|
||||||
|
Sienna Color = 0xffa0522d
|
||||||
|
// SkyBlue color constant
|
||||||
|
SkyBlue Color = 0xff87ceeb
|
||||||
|
// SlateBlue color constant
|
||||||
|
SlateBlue Color = 0xff6a5acd
|
||||||
|
// SlateGray color constant
|
||||||
|
SlateGray Color = 0xff708090
|
||||||
|
// SlateGrey color constant
|
||||||
|
SlateGrey Color = 0xff708090
|
||||||
|
// Snow color constant
|
||||||
|
Snow Color = 0xfffffafa
|
||||||
|
// SpringGreen color constant
|
||||||
|
SpringGreen Color = 0xff00ff7f
|
||||||
|
// SteelBlue color constant
|
||||||
|
SteelBlue Color = 0xff4682b4
|
||||||
|
// Tan color constant
|
||||||
|
Tan Color = 0xffd2b48c
|
||||||
|
// Thistle color constant
|
||||||
|
Thistle Color = 0xffd8bfd8
|
||||||
|
// Tomato color constant
|
||||||
|
Tomato Color = 0xffff6347
|
||||||
|
// Turquoise color constant
|
||||||
|
Turquoise Color = 0xff40e0d0
|
||||||
|
// Violet color constant
|
||||||
|
Violet Color = 0xffee82ee
|
||||||
|
// Wheat color constant
|
||||||
|
Wheat Color = 0xfff5deb3
|
||||||
|
// Whitesmoke color constant
|
||||||
|
Whitesmoke Color = 0xfff5f5f5
|
||||||
|
// YellowGreen color constant
|
||||||
|
YellowGreen Color = 0xff9acd32
|
||||||
|
)
|
||||||
|
|
||||||
|
var colorConstants = map[string]Color{
|
||||||
|
"black": 0xff000000,
|
||||||
|
"silver": 0xffc0c0c0,
|
||||||
|
"gray": 0xff808080,
|
||||||
|
"white": 0xffffffff,
|
||||||
|
"maroon": 0xff800000,
|
||||||
|
"red": 0xffff0000,
|
||||||
|
"purple": 0xff800080,
|
||||||
|
"fuchsia": 0xffff00ff,
|
||||||
|
"green": 0xff008000,
|
||||||
|
"lime": 0xff00ff00,
|
||||||
|
"olive": 0xff808000,
|
||||||
|
"yellow": 0xffffff00,
|
||||||
|
"navy": 0xff000080,
|
||||||
|
"blue": 0xff0000ff,
|
||||||
|
"teal": 0xff008080,
|
||||||
|
"aqua": 0xff00ffff,
|
||||||
|
"orange": 0xffffa500,
|
||||||
|
"aliceblue": 0xfff0f8ff,
|
||||||
|
"antiquewhite": 0xfffaebd7,
|
||||||
|
"aquamarine": 0xff7fffd4,
|
||||||
|
"azure": 0xfff0ffff,
|
||||||
|
"beige": 0xfff5f5dc,
|
||||||
|
"bisque": 0xffffe4c4,
|
||||||
|
"blanchedalmond": 0xffffebcd,
|
||||||
|
"blueviolet": 0xff8a2be2,
|
||||||
|
"brown": 0xffa52a2a,
|
||||||
|
"burlywood": 0xffdeb887,
|
||||||
|
"cadetblue": 0xff5f9ea0,
|
||||||
|
"chartreuse": 0xff7fff00,
|
||||||
|
"chocolate": 0xffd2691e,
|
||||||
|
"coral": 0xffff7f50,
|
||||||
|
"cornflowerblue": 0xff6495ed,
|
||||||
|
"cornsilk": 0xfffff8dc,
|
||||||
|
"crimson": 0xffdc143c,
|
||||||
|
"cyan": 0xff00ffff,
|
||||||
|
"darkblue": 0xff00008b,
|
||||||
|
"darkcyan": 0xff008b8b,
|
||||||
|
"darkgoldenrod": 0xffb8860b,
|
||||||
|
"darkgray": 0xffa9a9a9,
|
||||||
|
"darkgreen": 0xff006400,
|
||||||
|
"darkgrey": 0xffa9a9a9,
|
||||||
|
"darkkhaki": 0xffbdb76b,
|
||||||
|
"darkmagenta": 0xff8b008b,
|
||||||
|
"darkolivegreen": 0xff556b2f,
|
||||||
|
"darkorange": 0xffff8c00,
|
||||||
|
"darkorchid": 0xff9932cc,
|
||||||
|
"darkred": 0xff8b0000,
|
||||||
|
"darksalmon": 0xffe9967a,
|
||||||
|
"darkseagreen": 0xff8fbc8f,
|
||||||
|
"darkslateblue": 0xff483d8b,
|
||||||
|
"darkslategray": 0xff2f4f4f,
|
||||||
|
"darkslategrey": 0xff2f4f4f,
|
||||||
|
"darkturquoise": 0xff00ced1,
|
||||||
|
"darkviolet": 0xff9400d3,
|
||||||
|
"deeppink": 0xffff1493,
|
||||||
|
"deepskyblue": 0xff00bfff,
|
||||||
|
"dimgray": 0xff696969,
|
||||||
|
"dimgrey": 0xff696969,
|
||||||
|
"dodgerblue": 0xff1e90ff,
|
||||||
|
"firebrick": 0xffb22222,
|
||||||
|
"floralwhite": 0xfffffaf0,
|
||||||
|
"forestgreen": 0xff228b22,
|
||||||
|
"gainsboro": 0xffdcdcdc,
|
||||||
|
"ghostwhite": 0xfff8f8ff,
|
||||||
|
"gold": 0xffffd700,
|
||||||
|
"goldenrod": 0xffdaa520,
|
||||||
|
"greenyellow": 0xffadff2f,
|
||||||
|
"grey": 0xff808080,
|
||||||
|
"honeydew": 0xfff0fff0,
|
||||||
|
"hotpink": 0xffff69b4,
|
||||||
|
"indianred": 0xffcd5c5c,
|
||||||
|
"indigo": 0xff4b0082,
|
||||||
|
"ivory": 0xfffffff0,
|
||||||
|
"khaki": 0xfff0e68c,
|
||||||
|
"lavender": 0xffe6e6fa,
|
||||||
|
"lavenderblush": 0xfffff0f5,
|
||||||
|
"lawngreen": 0xff7cfc00,
|
||||||
|
"lemonchiffon": 0xfffffacd,
|
||||||
|
"lightblue": 0xffadd8e6,
|
||||||
|
"lightcoral": 0xfff08080,
|
||||||
|
"lightcyan": 0xffe0ffff,
|
||||||
|
"lightgoldenrodyellow": 0xfffafad2,
|
||||||
|
"lightgray": 0xffd3d3d3,
|
||||||
|
"lightgreen": 0xff90ee90,
|
||||||
|
"lightgrey": 0xffd3d3d3,
|
||||||
|
"lightpink": 0xffffb6c1,
|
||||||
|
"lightsalmon": 0xffffa07a,
|
||||||
|
"lightseagreen": 0xff20b2aa,
|
||||||
|
"lightskyblue": 0xff87cefa,
|
||||||
|
"lightslategray": 0xff778899,
|
||||||
|
"lightslategrey": 0xff778899,
|
||||||
|
"lightsteelblue": 0xffb0c4de,
|
||||||
|
"lightyellow": 0xffffffe0,
|
||||||
|
"limegreen": 0xff32cd32,
|
||||||
|
"linen": 0xfffaf0e6,
|
||||||
|
"magenta": 0xffff00ff,
|
||||||
|
"mediumaquamarine": 0xff66cdaa,
|
||||||
|
"mediumblue": 0xff0000cd,
|
||||||
|
"mediumorchid": 0xffba55d3,
|
||||||
|
"mediumpurple": 0xff9370db,
|
||||||
|
"mediumseagreen": 0xff3cb371,
|
||||||
|
"mediumslateblue": 0xff7b68ee,
|
||||||
|
"mediumspringgreen": 0xff00fa9a,
|
||||||
|
"mediumturquoise": 0xff48d1cc,
|
||||||
|
"mediumvioletred": 0xffc71585,
|
||||||
|
"midnightblue": 0xff191970,
|
||||||
|
"mintcream": 0xfff5fffa,
|
||||||
|
"mistyrose": 0xffffe4e1,
|
||||||
|
"moccasin": 0xffffe4b5,
|
||||||
|
"navajowhite": 0xffffdead,
|
||||||
|
"oldlace": 0xfffdf5e6,
|
||||||
|
"olivedrab": 0xff6b8e23,
|
||||||
|
"orangered": 0xffff4500,
|
||||||
|
"orchid": 0xffda70d6,
|
||||||
|
"palegoldenrod": 0xffeee8aa,
|
||||||
|
"palegreen": 0xff98fb98,
|
||||||
|
"paleturquoise": 0xffafeeee,
|
||||||
|
"palevioletred": 0xffdb7093,
|
||||||
|
"papayawhip": 0xffffefd5,
|
||||||
|
"peachpuff": 0xffffdab9,
|
||||||
|
"peru": 0xffcd853f,
|
||||||
|
"pink": 0xffffc0cb,
|
||||||
|
"plum": 0xffdda0dd,
|
||||||
|
"powderblue": 0xffb0e0e6,
|
||||||
|
"rosybrown": 0xffbc8f8f,
|
||||||
|
"royalblue": 0xff4169e1,
|
||||||
|
"saddlebrown": 0xff8b4513,
|
||||||
|
"salmon": 0xfffa8072,
|
||||||
|
"sandybrown": 0xfff4a460,
|
||||||
|
"seagreen": 0xff2e8b57,
|
||||||
|
"seashell": 0xfffff5ee,
|
||||||
|
"sienna": 0xffa0522d,
|
||||||
|
"skyblue": 0xff87ceeb,
|
||||||
|
"slateblue": 0xff6a5acd,
|
||||||
|
"slategray": 0xff708090,
|
||||||
|
"slategrey": 0xff708090,
|
||||||
|
"snow": 0xfffffafa,
|
||||||
|
"springgreen": 0xff00ff7f,
|
||||||
|
"steelblue": 0xff4682b4,
|
||||||
|
"tan": 0xffd2b48c,
|
||||||
|
"thistle": 0xffd8bfd8,
|
||||||
|
"tomato": 0xffff6347,
|
||||||
|
"turquoise": 0xff40e0d0,
|
||||||
|
"violet": 0xffee82ee,
|
||||||
|
"wheat": 0xfff5deb3,
|
||||||
|
"whitesmoke": 0xfff5f5f5,
|
||||||
|
"yellowgreen": 0xff9acd32,
|
||||||
|
}
|
|
@ -0,0 +1,253 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ColorChangedEvent = "color-changed"
|
||||||
|
ColorPickerValue = "color-picker-value"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColorPicker - ColorPicker view
|
||||||
|
type ColorPicker interface {
|
||||||
|
View
|
||||||
|
}
|
||||||
|
|
||||||
|
type colorPickerData struct {
|
||||||
|
viewData
|
||||||
|
colorChangedListeners []func(ColorPicker, Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorPicker create new ColorPicker object and return it
|
||||||
|
func NewColorPicker(session Session, params Params) ColorPicker {
|
||||||
|
view := new(colorPickerData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newColorPicker(session Session) View {
|
||||||
|
return NewColorPicker(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) Init(session Session) {
|
||||||
|
picker.viewData.Init(session)
|
||||||
|
picker.tag = "ColorPicker"
|
||||||
|
picker.colorChangedListeners = []func(ColorPicker, Color){}
|
||||||
|
picker.properties[Padding] = Px(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case Value, ColorProperty:
|
||||||
|
return ColorPickerValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) Remove(tag string) {
|
||||||
|
picker.remove(picker.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case ColorChangedEvent:
|
||||||
|
picker.colorChangedListeners = []func(ColorPicker, Color){}
|
||||||
|
|
||||||
|
case ColorPickerValue:
|
||||||
|
oldColor := GetColorPickerValue(picker, "")
|
||||||
|
delete(picker.properties, ColorPickerValue)
|
||||||
|
picker.colorChanged(oldColor)
|
||||||
|
|
||||||
|
default:
|
||||||
|
picker.viewData.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) Set(tag string, value interface{}) bool {
|
||||||
|
return picker.set(picker.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
picker.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case ColorChangedEvent:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(ColorPicker, Color):
|
||||||
|
picker.colorChangedListeners = []func(ColorPicker, Color){value}
|
||||||
|
|
||||||
|
case func(Color):
|
||||||
|
fn := func(view ColorPicker, date Color) {
|
||||||
|
value(date)
|
||||||
|
}
|
||||||
|
picker.colorChangedListeners = []func(ColorPicker, Color){fn}
|
||||||
|
|
||||||
|
case []func(ColorPicker, Color):
|
||||||
|
picker.colorChangedListeners = value
|
||||||
|
|
||||||
|
case []func(Color):
|
||||||
|
listeners := make([]func(ColorPicker, Color), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[i] = func(view ColorPicker, date Color) {
|
||||||
|
val(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picker.colorChangedListeners = listeners
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
listeners := make([]func(ColorPicker, Color), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := val.(type) {
|
||||||
|
case func(ColorPicker, Color):
|
||||||
|
listeners[i] = val
|
||||||
|
|
||||||
|
case func(Color):
|
||||||
|
listeners[i] = func(view ColorPicker, date Color) {
|
||||||
|
val(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picker.colorChangedListeners = listeners
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case ColorPickerValue:
|
||||||
|
oldColor := GetColorPickerValue(picker, "")
|
||||||
|
if picker.setColorProperty(ColorPickerValue, value) {
|
||||||
|
newValue := GetColorPickerValue(picker, "")
|
||||||
|
if oldColor != newValue {
|
||||||
|
picker.colorChanged(oldColor)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return picker.viewData.set(tag, value)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) colorChanged(oldColor Color) {
|
||||||
|
newColor := GetColorPickerValue(picker, "")
|
||||||
|
if oldColor != newColor {
|
||||||
|
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), newColor.rgbString()))
|
||||||
|
for _, listener := range picker.colorChangedListeners {
|
||||||
|
listener(picker, newColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) Get(tag string) interface{} {
|
||||||
|
return picker.get(picker.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) get(tag string) interface{} {
|
||||||
|
switch tag {
|
||||||
|
case ColorChangedEvent:
|
||||||
|
return picker.colorChangedListeners
|
||||||
|
|
||||||
|
default:
|
||||||
|
return picker.viewData.get(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) htmlTag() string {
|
||||||
|
return "input"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
picker.viewData.htmlProperties(self, buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(` type="color" value="`)
|
||||||
|
buffer.WriteString(GetColorPickerValue(picker, "").rgbString())
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
|
||||||
|
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||||
|
if IsDisabled(self) {
|
||||||
|
buffer.WriteString(` disabled`)
|
||||||
|
}
|
||||||
|
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
switch command {
|
||||||
|
case "textChanged":
|
||||||
|
if text, ok := data.PropertyValue("text"); ok {
|
||||||
|
oldColor := GetColorPickerValue(picker, "")
|
||||||
|
if color, ok := StringToColor(text); ok {
|
||||||
|
picker.properties[ColorPickerValue] = color
|
||||||
|
if color != oldColor {
|
||||||
|
for _, listener := range picker.colorChangedListeners {
|
||||||
|
listener(picker, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return picker.viewData.handleCommand(self, command, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorPickerValue returns the value of ColorPicker subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetColorPickerValue(view View, subviewID string) Color {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := colorStyledProperty(view, ColorPickerValue); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for _, tag := range []string{Value, ColorProperty} {
|
||||||
|
if value, ok := valueFromStyle(view, tag); ok {
|
||||||
|
if result, ok := valueToColor(value, view.Session()); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview.
|
||||||
|
// If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetColorChangedListeners(view View, subviewID string) []func(ColorPicker, Color) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(ColorChangedEvent); value != nil {
|
||||||
|
if listeners, ok := value.([]func(ColorPicker, Color)); ok {
|
||||||
|
return listeners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(ColorPicker, Color){}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestColorARGB(t *testing.T) {
|
||||||
|
color := Color(0x7FFE8743)
|
||||||
|
a, r, g, b := color.ARGB()
|
||||||
|
if a != 0x7F {
|
||||||
|
t.Error("a != 0x7F")
|
||||||
|
}
|
||||||
|
if r != 0xFE {
|
||||||
|
t.Error("r != 0xFE")
|
||||||
|
}
|
||||||
|
if g != 0x87 {
|
||||||
|
t.Error("g != 0x87")
|
||||||
|
}
|
||||||
|
if b != 0x43 {
|
||||||
|
t.Error("b != 0x43")
|
||||||
|
}
|
||||||
|
|
||||||
|
if color.Alpha() != 0x7F {
|
||||||
|
t.Error("color.Alpha() != 0x7F")
|
||||||
|
}
|
||||||
|
|
||||||
|
if color.Red() != 0xFE {
|
||||||
|
t.Error("color.Red() != 0xFE")
|
||||||
|
}
|
||||||
|
|
||||||
|
if color.Green() != 0x87 {
|
||||||
|
t.Error("color.Green() != 0x87")
|
||||||
|
}
|
||||||
|
|
||||||
|
if color.Blue() != 0x43 {
|
||||||
|
t.Error("color.Blue() != 0x43")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorSetValue(t *testing.T) {
|
||||||
|
createTestLog(t, true)
|
||||||
|
|
||||||
|
testData := []struct{ src, result string }{
|
||||||
|
{"#7F102040", "rgba(16,32,64,.50)"},
|
||||||
|
{"#102040", "rgb(16,32,64)"},
|
||||||
|
{"#8124", "rgba(17,34,68,.53)"},
|
||||||
|
{"rgba(17,34,67,.5)", "rgba(17,34,67,.50)"},
|
||||||
|
{"rgb(.25,50%,96)", "rgb(63,127,96)"},
|
||||||
|
{"rgba(.25,50%,96,100%)", "rgb(63,127,96)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range testData {
|
||||||
|
color, ok := StringToColor(data.src)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf(`color.SetValue("%s") fail`, data.src)
|
||||||
|
}
|
||||||
|
result := color.cssString()
|
||||||
|
if result != data.result {
|
||||||
|
t.Errorf(`color.cssString() = "%s", expected: "%s"`, result, data.result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorWriteData(t *testing.T) {
|
||||||
|
testCSS := func(t *testing.T, color Color, result string) {
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
buffer.WriteString(color.cssString())
|
||||||
|
str := buffer.String()
|
||||||
|
if str != result {
|
||||||
|
t.Errorf("color = %#X, expected = \"%s\", result = \"%s\"", color, result, str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
color := Color(0x7FFE8743)
|
||||||
|
color.writeData(buffer)
|
||||||
|
str := buffer.String()
|
||||||
|
if str != "#7FFE8743" {
|
||||||
|
t.Errorf(`color = %#X, expected = "#7FFE8743", result = "%s"`, color, str)
|
||||||
|
}
|
||||||
|
|
||||||
|
testCSS(t, Color(0x7FFE8743), "rgba(254,135,67,.50)")
|
||||||
|
testCSS(t, Color(0xFFFE8743), "rgb(254,135,67)")
|
||||||
|
testCSS(t, Color(0x05FE8743), "rgba(254,135,67,.02)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorSetData(t *testing.T) {
|
||||||
|
test := func(t *testing.T, data string, result Color) {
|
||||||
|
color, ok := StringToColor(data)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("data = \"%s\", fail result", data)
|
||||||
|
} else if color != result {
|
||||||
|
t.Errorf("data = \"%s\", expected = %#X, result = %#X", data, result, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test(t, "#7Ffe8743", 0x7FFE8743)
|
||||||
|
test(t, "#fE8743", 0xFFFE8743)
|
||||||
|
test(t, "#AE43", 0xAAEE4433)
|
||||||
|
test(t, "#E43", 0xFFEE4433)
|
||||||
|
|
||||||
|
failData := []string{
|
||||||
|
"",
|
||||||
|
"7FfeG743",
|
||||||
|
"#7Ffe87439",
|
||||||
|
"#7FfeG743",
|
||||||
|
"#7Ffe874",
|
||||||
|
"#feG743",
|
||||||
|
"#7Ffe8",
|
||||||
|
"#fG73",
|
||||||
|
"#GF3",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, data := range failData {
|
||||||
|
if color, ok := StringToColor(data); ok {
|
||||||
|
t.Errorf("data = \"%s\", success, result = %#X", data, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,222 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ColumnCount is the constant for the "column-count" property tag.
|
||||||
|
// The "column-count" int property specifies number of columns into which the content is break
|
||||||
|
// Values less than zero are not valid. if the "column-count" property value is 0 then
|
||||||
|
// the number of columns is calculated based on the "column-width" property
|
||||||
|
ColumnCount = "column-count"
|
||||||
|
// ColumnWidth is the constant for the "column-width" property tag.
|
||||||
|
// The "column-width" SizeUnit property specifies the width of each column.
|
||||||
|
ColumnWidth = "column-width"
|
||||||
|
// ColumnGap is the constant for the "column-gap" property tag.
|
||||||
|
// The "column-width" SizeUnit property sets the size of the gap (gutter) between columns.
|
||||||
|
ColumnGap = "column-gap"
|
||||||
|
// ColumnSeparator is the constant for the "column-separator" property tag.
|
||||||
|
// The "column-separator" property specifies the line drawn between columns in a multi-column layout.
|
||||||
|
ColumnSeparator = "column-separator"
|
||||||
|
// ColumnSeparatorStyle is the constant for the "column-separator-style" property tag.
|
||||||
|
// The "column-separator-style" int property sets the style of the line drawn between
|
||||||
|
// columns in a multi-column layout.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
ColumnSeparatorStyle = "column-separator-style"
|
||||||
|
// ColumnSeparatorWidth is the constant for the "column-separator-width" property tag.
|
||||||
|
// The "column-separator-width" SizeUnit property sets the width of the line drawn between
|
||||||
|
// columns in a multi-column layout.
|
||||||
|
ColumnSeparatorWidth = "column-separator-width"
|
||||||
|
// ColumnSeparatorColor is the constant for the "column-separator-color" property tag.
|
||||||
|
// The "column-separator-color" Color property sets the color of the line drawn between
|
||||||
|
// columns in a multi-column layout.
|
||||||
|
ColumnSeparatorColor = "column-separator-color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColumnLayout - grid-container of View
|
||||||
|
type ColumnLayout interface {
|
||||||
|
ViewsContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type columnLayoutData struct {
|
||||||
|
viewsContainerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColumnLayout create new ColumnLayout object and return it
|
||||||
|
func NewColumnLayout(session Session, params Params) ColumnLayout {
|
||||||
|
view := new(columnLayoutData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newColumnLayout(session Session) View {
|
||||||
|
return NewColumnLayout(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of ColumnLayout by default values
|
||||||
|
func (ColumnLayout *columnLayoutData) Init(session Session) {
|
||||||
|
ColumnLayout.viewsContainerData.Init(session)
|
||||||
|
ColumnLayout.tag = "ColumnLayout"
|
||||||
|
//ColumnLayout.systemClass = "ruiColumnLayout"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (columnLayout *columnLayoutData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case Gap:
|
||||||
|
return ColumnGap
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (columnLayout *columnLayoutData) Get(tag string) interface{} {
|
||||||
|
return columnLayout.get(columnLayout.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (columnLayout *columnLayoutData) Remove(tag string) {
|
||||||
|
columnLayout.remove(columnLayout.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (columnLayout *columnLayoutData) remove(tag string) {
|
||||||
|
columnLayout.viewsContainerData.remove(tag)
|
||||||
|
switch tag {
|
||||||
|
case ColumnCount, ColumnWidth, ColumnGap:
|
||||||
|
updateCSSProperty(columnLayout.htmlID(), tag, "", columnLayout.Session())
|
||||||
|
|
||||||
|
case ColumnSeparator:
|
||||||
|
updateCSSProperty(columnLayout.htmlID(), "column-rule", "", columnLayout.Session())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (columnLayout *columnLayoutData) Set(tag string, value interface{}) bool {
|
||||||
|
return columnLayout.set(columnLayout.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (columnLayout *columnLayoutData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
columnLayout.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case ColumnCount:
|
||||||
|
if columnLayout.setIntProperty(tag, value) {
|
||||||
|
session := columnLayout.Session()
|
||||||
|
if count, ok := intProperty(columnLayout, tag, session, 0); ok && count > 0 {
|
||||||
|
updateCSSProperty(columnLayout.htmlID(), tag, strconv.Itoa(count), session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(columnLayout.htmlID(), tag, "auto", session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := columnLayout.viewsContainerData.set(tag, value)
|
||||||
|
if ok {
|
||||||
|
switch tag {
|
||||||
|
case ColumnSeparator:
|
||||||
|
css := ""
|
||||||
|
session := columnLayout.Session()
|
||||||
|
if val, ok := columnLayout.properties[ColumnSeparator]; ok {
|
||||||
|
separator := val.(ColumnSeparatorProperty)
|
||||||
|
css = separator.cssValue(columnLayout.Session())
|
||||||
|
}
|
||||||
|
updateCSSProperty(columnLayout.htmlID(), "column-rule", css, session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumnCount returns int value which specifies number of columns into which the content of
|
||||||
|
// ColumnLayout is break. If the return value is 0 then the number of columns is calculated
|
||||||
|
// based on the "column-width" property.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetColumnCount(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
result, _ := intStyledProperty(view, ColumnCount, 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumnWidth returns SizeUnit value which specifies the width of each column of ColumnLayout.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetColumnWidth(view View, subviewID string) SizeUnit {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return AutoSize()
|
||||||
|
}
|
||||||
|
result, _ := sizeStyledProperty(view, ColumnWidth)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumnGap returns SizeUnit property which specifies the size of the gap (gutter) between columns of ColumnLayout.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetColumnGap(view View, subviewID string) SizeUnit {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return AutoSize()
|
||||||
|
}
|
||||||
|
result, _ := sizeStyledProperty(view, ColumnGap)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetColumnSeparator returns ViewBorder struct which specifies the line drawn between
|
||||||
|
// columns in a multi-column ColumnLayout.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetColumnSeparator(view View, subviewID string) ViewBorder {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if view != nil {
|
||||||
|
value := view.Get(ColumnSeparator)
|
||||||
|
if value == nil {
|
||||||
|
value, _ = valueFromStyle(view, ColumnSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != nil {
|
||||||
|
if separator, ok := value.(ColumnSeparatorProperty); ok {
|
||||||
|
return separator.ViewBorder(view.Session())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ViewBorder{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnSeparatorStyle returns int value which specifies the style of the line drawn between
|
||||||
|
// columns in a multi-column layout.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetColumnSeparatorStyle(view View, subviewID string) int {
|
||||||
|
border := GetColumnSeparator(view, subviewID)
|
||||||
|
return border.Style
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnSeparatorWidth returns SizeUnit value which specifies the width of the line drawn between
|
||||||
|
// columns in a multi-column layout.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetColumnSeparatorWidth(view View, subviewID string) SizeUnit {
|
||||||
|
border := GetColumnSeparator(view, subviewID)
|
||||||
|
return border.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnSeparatorColor returns Color value which specifies the color of the line drawn between
|
||||||
|
// columns in a multi-column layout.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetColumnSeparatorColor(view View, subviewID string) Color {
|
||||||
|
border := GetColumnSeparator(view, subviewID)
|
||||||
|
return border.Color
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColumnSeparatorProperty is the interface of a view separator data
|
||||||
|
type ColumnSeparatorProperty interface {
|
||||||
|
Properties
|
||||||
|
ruiStringer
|
||||||
|
fmt.Stringer
|
||||||
|
ViewBorder(session Session) ViewBorder
|
||||||
|
cssValue(session Session) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type columnSeparatorProperty struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
func newColumnSeparatorProperty(value interface{}) ColumnSeparatorProperty {
|
||||||
|
|
||||||
|
if value == nil {
|
||||||
|
separator := new(columnSeparatorProperty)
|
||||||
|
separator.properties = map[string]interface{}{}
|
||||||
|
return separator
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case ColumnSeparatorProperty:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
separator := new(columnSeparatorProperty)
|
||||||
|
separator.properties = map[string]interface{}{}
|
||||||
|
for _, tag := range []string{Style, Width, ColorProperty} {
|
||||||
|
if val, ok := value.PropertyValue(tag); ok && val != "" {
|
||||||
|
separator.set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return separator
|
||||||
|
|
||||||
|
case ViewBorder:
|
||||||
|
separator := new(columnSeparatorProperty)
|
||||||
|
separator.properties = map[string]interface{}{
|
||||||
|
Style: value.Style,
|
||||||
|
Width: value.Width,
|
||||||
|
ColorProperty: value.Color,
|
||||||
|
}
|
||||||
|
return separator
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidPropertyValue(Border, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColumnSeparator creates the new ColumnSeparatorProperty
|
||||||
|
func NewColumnSeparator(params Params) ColumnSeparatorProperty {
|
||||||
|
separator := new(columnSeparatorProperty)
|
||||||
|
separator.properties = map[string]interface{}{}
|
||||||
|
if params != nil {
|
||||||
|
for _, tag := range []string{Style, Width, ColorProperty} {
|
||||||
|
if value, ok := params[tag]; ok && value != nil {
|
||||||
|
separator.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return separator
|
||||||
|
}
|
||||||
|
|
||||||
|
func (separator *columnSeparatorProperty) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case ColumnSeparatorStyle, "separator-style":
|
||||||
|
return Style
|
||||||
|
|
||||||
|
case ColumnSeparatorWidth, "separator-width":
|
||||||
|
return Width
|
||||||
|
|
||||||
|
case ColumnSeparatorColor, "separator-color":
|
||||||
|
return ColorProperty
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (separator *columnSeparatorProperty) ruiString(writer ruiWriter) {
|
||||||
|
writer.startObject("_")
|
||||||
|
for _, tag := range []string{Style, Width, ColorProperty} {
|
||||||
|
if value, ok := separator.properties[tag]; ok {
|
||||||
|
writer.writeProperty(Style, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (separator *columnSeparatorProperty) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
separator.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (separator *columnSeparatorProperty) Remove(tag string) {
|
||||||
|
|
||||||
|
switch tag = separator.normalizeTag(tag); tag {
|
||||||
|
case Style, Width, ColorProperty:
|
||||||
|
delete(separator.properties, tag)
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (separator *columnSeparatorProperty) Set(tag string, value interface{}) bool {
|
||||||
|
tag = separator.normalizeTag(tag)
|
||||||
|
|
||||||
|
if value == nil {
|
||||||
|
separator.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Style:
|
||||||
|
return separator.setEnumProperty(Style, value, enumProperties[BorderStyle].values)
|
||||||
|
|
||||||
|
case Width:
|
||||||
|
return separator.setSizeProperty(Width, value)
|
||||||
|
|
||||||
|
case ColorProperty:
|
||||||
|
return separator.setColorProperty(ColorProperty, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (separator *columnSeparatorProperty) Get(tag string) interface{} {
|
||||||
|
tag = separator.normalizeTag(tag)
|
||||||
|
|
||||||
|
if result, ok := separator.properties[tag]; ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (separator *columnSeparatorProperty) ViewBorder(session Session) ViewBorder {
|
||||||
|
style, _ := valueToEnum(separator.getRaw(Style), BorderStyle, session, NoneLine)
|
||||||
|
width, _ := sizeProperty(separator, Width, session)
|
||||||
|
color, _ := colorProperty(separator, ColorProperty, session)
|
||||||
|
|
||||||
|
return ViewBorder{
|
||||||
|
Style: style,
|
||||||
|
Width: width,
|
||||||
|
Color: color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (separator *columnSeparatorProperty) cssValue(session Session) string {
|
||||||
|
value := separator.ViewBorder(session)
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
if value.Width.Type != Auto && value.Width.Type != SizeInFraction && value.Width.Value > 0 {
|
||||||
|
buffer.WriteString(value.Width.cssString(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
styles := enumProperties[BorderStyle].cssValues
|
||||||
|
if value.Style > 0 && value.Style < len(styles) {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(styles[value.Style])
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.Color != 0 {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(value.Color.cssString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var systemStyles = map[string]string{
|
||||||
|
"ruiApp": "body",
|
||||||
|
"ruiDefault": "div",
|
||||||
|
"ruiArticle": "article",
|
||||||
|
"ruiSection": "section",
|
||||||
|
"ruiAside": "aside",
|
||||||
|
"ruiHeader": "header",
|
||||||
|
"ruiMain": "main",
|
||||||
|
"ruiFooter": "footer",
|
||||||
|
"ruiNavigation": "nav",
|
||||||
|
"ruiFigure": "figure",
|
||||||
|
"ruiFigureCaption": "figcaption",
|
||||||
|
"ruiButton": "button",
|
||||||
|
"ruiP": "p",
|
||||||
|
"ruiParagraph": "p",
|
||||||
|
"ruiH1": "h1",
|
||||||
|
"ruiH2": "h2",
|
||||||
|
"ruiH3": "h3",
|
||||||
|
"ruiH4": "h4",
|
||||||
|
"ruiH5": "h5",
|
||||||
|
"ruiH6": "h6",
|
||||||
|
"ruiBlockquote": "blockquote",
|
||||||
|
"ruiCode": "code",
|
||||||
|
"ruiTable": "table",
|
||||||
|
"ruiTableHead": "thead",
|
||||||
|
"ruiTableFoot": "tfoot",
|
||||||
|
"ruiTableRow": "tr",
|
||||||
|
"ruiTableColumn": "col",
|
||||||
|
"ruiTableCell": "td",
|
||||||
|
"ruiDropDownList": "select",
|
||||||
|
"ruiDropDownListItem": "option",
|
||||||
|
}
|
||||||
|
|
||||||
|
var disabledStyles = []string{
|
||||||
|
"ruiRoot",
|
||||||
|
"ruiPopupLayer",
|
||||||
|
"ruiAbsoluteLayout",
|
||||||
|
"ruiGridLayout",
|
||||||
|
"ruiListLayout",
|
||||||
|
"ruiStackLayout",
|
||||||
|
"ruiStackPageLayout",
|
||||||
|
"ruiTabsLayout",
|
||||||
|
"ruiImageView",
|
||||||
|
"ruiListView",
|
||||||
|
}
|
||||||
|
|
||||||
|
type cssBuilder interface {
|
||||||
|
add(key, value string)
|
||||||
|
addValues(key, separator string, values ...string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type viewCSSBuilder struct {
|
||||||
|
buffer *strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
type cssValueBuilder struct {
|
||||||
|
buffer *strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
type cssStyleBuilder struct {
|
||||||
|
buffer *strings.Builder
|
||||||
|
media bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *viewCSSBuilder) finish() string {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
result := builder.buffer.String()
|
||||||
|
freeStringBuilder(builder.buffer)
|
||||||
|
builder.buffer = nil
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *viewCSSBuilder) add(key, value string) {
|
||||||
|
if value != "" {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.buffer = allocStringBuilder()
|
||||||
|
} else if builder.buffer.Len() > 0 {
|
||||||
|
builder.buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.buffer.WriteString(key)
|
||||||
|
builder.buffer.WriteString(": ")
|
||||||
|
builder.buffer.WriteString(value)
|
||||||
|
builder.buffer.WriteRune(';')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *viewCSSBuilder) addValues(key, separator string, values ...string) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.buffer = allocStringBuilder()
|
||||||
|
} else if builder.buffer.Len() > 0 {
|
||||||
|
builder.buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.buffer.WriteString(key)
|
||||||
|
builder.buffer.WriteString(": ")
|
||||||
|
for i, value := range values {
|
||||||
|
if i > 0 {
|
||||||
|
builder.buffer.WriteString(separator)
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(value)
|
||||||
|
}
|
||||||
|
builder.buffer.WriteRune(';')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssValueBuilder) finish() string {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
result := builder.buffer.String()
|
||||||
|
freeStringBuilder(builder.buffer)
|
||||||
|
builder.buffer = nil
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssValueBuilder) add(key, value string) {
|
||||||
|
if value != "" {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.buffer = allocStringBuilder()
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssValueBuilder) addValues(key, separator string, values ...string) {
|
||||||
|
if len(values) > 0 {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.buffer = allocStringBuilder()
|
||||||
|
}
|
||||||
|
for i, value := range values {
|
||||||
|
if i > 0 {
|
||||||
|
builder.buffer.WriteString(separator)
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssStyleBuilder) init() {
|
||||||
|
builder.buffer = allocStringBuilder()
|
||||||
|
builder.buffer.Grow(16 * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssStyleBuilder) finish() string {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
result := builder.buffer.String()
|
||||||
|
freeStringBuilder(builder.buffer)
|
||||||
|
builder.buffer = nil
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssStyleBuilder) startMedia(rule string) {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.init()
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(`@media screen`)
|
||||||
|
builder.buffer.WriteString(rule)
|
||||||
|
builder.buffer.WriteString(` {\n`)
|
||||||
|
builder.media = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssStyleBuilder) endMedia() {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.init()
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(`}\n`)
|
||||||
|
builder.media = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssStyleBuilder) startStyle(name string) {
|
||||||
|
for _, disabledName := range disabledStyles {
|
||||||
|
if name == disabledName {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.init()
|
||||||
|
}
|
||||||
|
if builder.media {
|
||||||
|
builder.buffer.WriteString(`\t`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sysName, ok := systemStyles[name]; ok {
|
||||||
|
builder.buffer.WriteString(sysName)
|
||||||
|
} else {
|
||||||
|
builder.buffer.WriteRune('.')
|
||||||
|
builder.buffer.WriteString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.buffer.WriteString(` {\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssStyleBuilder) endStyle() {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.init()
|
||||||
|
}
|
||||||
|
if builder.media {
|
||||||
|
builder.buffer.WriteString(`\t`)
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(`}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssStyleBuilder) add(key, value string) {
|
||||||
|
if value != "" {
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.init()
|
||||||
|
}
|
||||||
|
if builder.media {
|
||||||
|
builder.buffer.WriteString(`\t`)
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(`\t`)
|
||||||
|
builder.buffer.WriteString(key)
|
||||||
|
builder.buffer.WriteString(`: `)
|
||||||
|
builder.buffer.WriteString(value)
|
||||||
|
builder.buffer.WriteString(`;\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (builder *cssStyleBuilder) addValues(key, separator string, values ...string) {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if builder.buffer == nil {
|
||||||
|
builder.init()
|
||||||
|
}
|
||||||
|
if builder.media {
|
||||||
|
builder.buffer.WriteString(`\t`)
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(`\t`)
|
||||||
|
builder.buffer.WriteString(key)
|
||||||
|
builder.buffer.WriteString(`: `)
|
||||||
|
for i, value := range values {
|
||||||
|
if i > 0 {
|
||||||
|
builder.buffer.WriteString(separator)
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(value)
|
||||||
|
}
|
||||||
|
builder.buffer.WriteString(`;\n`)
|
||||||
|
}
|
|
@ -0,0 +1,261 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// CustomView defines a custom view interface
|
||||||
|
type CustomView interface {
|
||||||
|
ViewsContainer
|
||||||
|
CreateSuperView(session Session) View
|
||||||
|
SuperView() View
|
||||||
|
setSuperView(view View)
|
||||||
|
setTag(tag string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomViewData defines a data of a basic custom view
|
||||||
|
type CustomViewData struct {
|
||||||
|
tag string
|
||||||
|
superView View
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitCustomView initializes fields of CustomView by default values
|
||||||
|
func InitCustomView(customView CustomView, tag string, session Session, params Params) bool {
|
||||||
|
customView.setTag(tag)
|
||||||
|
if view := customView.CreateSuperView(session); view != nil {
|
||||||
|
customView.setSuperView(view)
|
||||||
|
setInitParams(customView, params)
|
||||||
|
} else {
|
||||||
|
ErrorLog(`nil SuperView of "` + tag + `" view`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuperView returns a super view
|
||||||
|
func (customView *CustomViewData) SuperView() View {
|
||||||
|
return customView.superView
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) setSuperView(view View) {
|
||||||
|
customView.superView = view
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) setTag(tag string) {
|
||||||
|
customView.tag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a value of the property with name defined by the argument.
|
||||||
|
// The type of return value depends on the property. If the property is not set then nil is returned.
|
||||||
|
func (customView *CustomViewData) Get(tag string) interface{} {
|
||||||
|
return customView.superView.Get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) getRaw(tag string) interface{} {
|
||||||
|
return customView.superView.getRaw(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) setRaw(tag string, value interface{}) {
|
||||||
|
customView.superView.setRaw(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the value (second argument) of the property with name defined by the first argument.
|
||||||
|
// Return "true" if the value has been set, in the opposite case "false" are returned and
|
||||||
|
// a description of the error is written to the log
|
||||||
|
func (customView *CustomViewData) Set(tag string, value interface{}) bool {
|
||||||
|
return customView.superView.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) SetAnimated(tag string, value interface{}, animation Animation) bool {
|
||||||
|
return customView.superView.SetAnimated(tag, value, animation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes the property with name defined by the argument
|
||||||
|
func (customView *CustomViewData) Remove(tag string) {
|
||||||
|
customView.superView.Remove(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllTags returns an array of the set properties
|
||||||
|
func (customView *CustomViewData) AllTags() []string {
|
||||||
|
return customView.superView.AllTags()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear removes all properties
|
||||||
|
func (customView *CustomViewData) Clear() {
|
||||||
|
customView.superView.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes fields of View by default values
|
||||||
|
func (customView *CustomViewData) Init(session Session) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session returns a current Session interface
|
||||||
|
func (customView *CustomViewData) Session() Session {
|
||||||
|
return customView.superView.Session()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent returns a parent view
|
||||||
|
func (customView *CustomViewData) Parent() View {
|
||||||
|
return customView.superView.Parent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) parentHTMLID() string {
|
||||||
|
return customView.superView.parentHTMLID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) setParentID(parentID string) {
|
||||||
|
customView.superView.setParentID(parentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag returns a tag of View interface
|
||||||
|
func (customView *CustomViewData) Tag() string {
|
||||||
|
if customView.tag != "" {
|
||||||
|
return customView.tag
|
||||||
|
}
|
||||||
|
return customView.superView.Tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns a id of the view
|
||||||
|
func (customView *CustomViewData) ID() string {
|
||||||
|
return customView.superView.ID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focusable returns true if the view receives the focus
|
||||||
|
func (customView *CustomViewData) Focusable() bool {
|
||||||
|
return customView.superView.Focusable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// SetTransitionEndListener sets the new listener of the transition end event
|
||||||
|
func (customView *CustomViewData) SetTransitionEndListener(property string, listener TransitionEndListener) {
|
||||||
|
customView.superView.SetTransitionEndListener(property, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTransitionEndFunc sets the new listener function of the transition end event
|
||||||
|
func (customView *CustomViewData) SetTransitionEndFunc(property string, listenerFunc func(View, string)) {
|
||||||
|
customView.superView.SetTransitionEndFunc(property, listenerFunc)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Frame returns a location and size of the view in pixels
|
||||||
|
func (customView *CustomViewData) Frame() Frame {
|
||||||
|
return customView.superView.Frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) Scroll() Frame {
|
||||||
|
return customView.superView.Scroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) onResize(self View, x, y, width, height float64) {
|
||||||
|
customView.superView.onResize(customView.superView, x, y, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) onItemResize(self View, index int, x, y, width, height float64) {
|
||||||
|
customView.superView.onItemResize(customView.superView, index, x, y, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
return customView.superView.handleCommand(customView.superView, command, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) htmlClass(disabled bool) string {
|
||||||
|
return customView.superView.htmlClass(disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) htmlTag() string {
|
||||||
|
return customView.superView.htmlTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) closeHTMLTag() bool {
|
||||||
|
return customView.superView.closeHTMLTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) htmlID() string {
|
||||||
|
return customView.superView.htmlID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
customView.superView.htmlSubviews(customView.superView, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
customView.superView.htmlProperties(customView.superView, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||||
|
customView.superView.htmlDisabledProperties(customView.superView, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) {
|
||||||
|
customView.superView.cssStyle(customView.superView, builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) addToCSSStyle(addCSS map[string]string) {
|
||||||
|
customView.superView.addToCSSStyle(addCSS)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) setNoResizeEvent() {
|
||||||
|
customView.superView.setNoResizeEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) isNoResizeEvent() bool {
|
||||||
|
return customView.superView.isNoResizeEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Views return a list of child views
|
||||||
|
func (customView *CustomViewData) Views() []View {
|
||||||
|
if customView.superView != nil {
|
||||||
|
if container, ok := customView.superView.(ViewsContainer); ok {
|
||||||
|
return container.Views()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []View{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append appends a view to the end of the list of a view children
|
||||||
|
func (customView *CustomViewData) Append(view View) {
|
||||||
|
if customView.superView != nil {
|
||||||
|
if container, ok := customView.superView.(ViewsContainer); ok {
|
||||||
|
container.Append(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts a view to the "index" position in the list of a view children
|
||||||
|
func (customView *CustomViewData) Insert(view View, index uint) {
|
||||||
|
if customView.superView != nil {
|
||||||
|
if container, ok := customView.superView.(ViewsContainer); ok {
|
||||||
|
container.Insert(view, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes a view from the list of a view children and return it
|
||||||
|
func (customView *CustomViewData) RemoveView(index uint) View {
|
||||||
|
if customView.superView != nil {
|
||||||
|
if container, ok := customView.superView.(ViewsContainer); ok {
|
||||||
|
return container.RemoveView(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) String() string {
|
||||||
|
if customView.superView != nil {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
customView.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
return customView.tag + " { }"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) ruiString(writer ruiWriter) {
|
||||||
|
if customView.superView != nil {
|
||||||
|
ruiViewString(customView.superView, customView.tag, writer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (customView *CustomViewData) setScroll(x, y, width, height float64) {
|
||||||
|
if customView.superView != nil {
|
||||||
|
customView.superView.setScroll(x, y, width, height)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,631 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataValue interface of a data node value
|
||||||
|
type DataValue interface {
|
||||||
|
IsObject() bool
|
||||||
|
Object() DataObject
|
||||||
|
Value() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// DataObject interface of a data object
|
||||||
|
type DataObject interface {
|
||||||
|
DataValue
|
||||||
|
Tag() string
|
||||||
|
PropertyCount() int
|
||||||
|
Property(index int) DataNode
|
||||||
|
PropertyWithTag(tag string) DataNode
|
||||||
|
PropertyValue(tag string) (string, bool)
|
||||||
|
PropertyObject(tag string) DataObject
|
||||||
|
SetPropertyValue(tag, value string)
|
||||||
|
SetPropertyObject(tag string, object DataObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TextNode - node is the pair "tag - text value". Syntax: <tag> = <text>
|
||||||
|
TextNode = 0
|
||||||
|
// ObjectNode - node is the pair "tag - object". Syntax: <tag> = <object name>{...}
|
||||||
|
ObjectNode = 1
|
||||||
|
// ArrayNode - node is the pair "tag - object". Syntax: <tag> = [...]
|
||||||
|
ArrayNode = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// DataNode interface of a data node
|
||||||
|
type DataNode interface {
|
||||||
|
Tag() string
|
||||||
|
Type() int
|
||||||
|
Text() string
|
||||||
|
Object() DataObject
|
||||||
|
ArraySize() int
|
||||||
|
ArrayElement(index int) DataValue
|
||||||
|
ArrayElements() []DataValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
type dataStringValue struct {
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value *dataStringValue) Value() string {
|
||||||
|
return value.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value *dataStringValue) IsObject() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (value *dataStringValue) Object() DataObject {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
type dataObject struct {
|
||||||
|
tag string
|
||||||
|
property []DataNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDataObject create new DataObject with the tag and empty property list
|
||||||
|
func NewDataObject(tag string) DataObject {
|
||||||
|
obj := new(dataObject)
|
||||||
|
obj.tag = tag
|
||||||
|
obj.property = []DataNode{}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) Value() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) IsObject() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) Object() DataObject {
|
||||||
|
return object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) Tag() string {
|
||||||
|
return object.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) PropertyCount() int {
|
||||||
|
if object.property != nil {
|
||||||
|
return len(object.property)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) Property(index int) DataNode {
|
||||||
|
if object.property == nil || index < 0 || index >= len(object.property) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return object.property[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) PropertyWithTag(tag string) DataNode {
|
||||||
|
if object.property != nil {
|
||||||
|
for _, node := range object.property {
|
||||||
|
if node.Tag() == tag {
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) PropertyValue(tag string) (string, bool) {
|
||||||
|
if node := object.PropertyWithTag(tag); node != nil && node.Type() == TextNode {
|
||||||
|
return node.Text(), true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) PropertyObject(tag string) DataObject {
|
||||||
|
if node := object.PropertyWithTag(tag); node != nil && node.Type() == ObjectNode {
|
||||||
|
return node.Object()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (object *dataObject) setNode(node DataNode) {
|
||||||
|
if object.property == nil || len(object.property) == 0 {
|
||||||
|
object.property = []DataNode{node}
|
||||||
|
} else {
|
||||||
|
tag := node.Tag()
|
||||||
|
for i, p := range object.property {
|
||||||
|
if p.Tag() == tag {
|
||||||
|
object.property[i] = node
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object.property = append(object.property, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPropertyValue - set a string property with tag by value
|
||||||
|
func (object *dataObject) SetPropertyValue(tag, value string) {
|
||||||
|
val := new(dataStringValue)
|
||||||
|
val.value = value
|
||||||
|
node := new(dataNode)
|
||||||
|
node.tag = tag
|
||||||
|
node.value = val
|
||||||
|
object.setNode(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPropertyObject - set a property with tag by object
|
||||||
|
func (object *dataObject) SetPropertyObject(tag string, obj DataObject) {
|
||||||
|
node := new(dataNode)
|
||||||
|
node.tag = tag
|
||||||
|
node.value = obj
|
||||||
|
object.setNode(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
type dataNode struct {
|
||||||
|
tag string
|
||||||
|
value DataValue
|
||||||
|
array []DataValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *dataNode) Tag() string {
|
||||||
|
return node.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *dataNode) Type() int {
|
||||||
|
if node.array != nil {
|
||||||
|
return ArrayNode
|
||||||
|
}
|
||||||
|
if node.value.IsObject() {
|
||||||
|
return ObjectNode
|
||||||
|
}
|
||||||
|
return TextNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *dataNode) Text() string {
|
||||||
|
if node.value != nil {
|
||||||
|
return node.value.Value()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *dataNode) Object() DataObject {
|
||||||
|
if node.value != nil {
|
||||||
|
return node.value.Object()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *dataNode) ArraySize() int {
|
||||||
|
if node.array != nil {
|
||||||
|
return len(node.array)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *dataNode) ArrayElement(index int) DataValue {
|
||||||
|
if node.array != nil && index >= 0 && index < len(node.array) {
|
||||||
|
return node.array[index]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *dataNode) ArrayElements() []DataValue {
|
||||||
|
if node.array != nil {
|
||||||
|
return node.array
|
||||||
|
}
|
||||||
|
return []DataValue{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDataText - parse text and return DataNode
|
||||||
|
func ParseDataText(text string) DataObject {
|
||||||
|
|
||||||
|
if strings.ContainsAny(text, "\r") {
|
||||||
|
text = strings.Replace(text, "\r\n", "\n", -1)
|
||||||
|
text = strings.Replace(text, "\r", "\n", -1)
|
||||||
|
}
|
||||||
|
data := append([]rune(text), rune(0))
|
||||||
|
pos := 0
|
||||||
|
size := len(data) - 1
|
||||||
|
line := 1
|
||||||
|
lineStart := 0
|
||||||
|
|
||||||
|
skipSpaces := func(skipNewLine bool) {
|
||||||
|
for pos < size {
|
||||||
|
switch data[pos] {
|
||||||
|
case '\n':
|
||||||
|
if !skipNewLine {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
line++
|
||||||
|
lineStart = pos + 1
|
||||||
|
|
||||||
|
case '/':
|
||||||
|
if pos+1 < size {
|
||||||
|
switch data[pos+1] {
|
||||||
|
case '/':
|
||||||
|
pos += 2
|
||||||
|
for pos < size && data[pos] != '\n' {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
pos--
|
||||||
|
|
||||||
|
case '*':
|
||||||
|
pos += 3
|
||||||
|
for {
|
||||||
|
if pos >= size {
|
||||||
|
ErrorLog("Unexpected end of file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data[pos-1] == '*' && data[pos] == '/' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if data[pos-1] == '\n' {
|
||||||
|
line++
|
||||||
|
lineStart = pos
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case ' ', '\t':
|
||||||
|
// do nothing
|
||||||
|
|
||||||
|
default:
|
||||||
|
if !unicode.IsSpace(data[pos]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseTag := func() (string, bool) {
|
||||||
|
skipSpaces(true)
|
||||||
|
startPos := pos
|
||||||
|
if data[pos] == '`' {
|
||||||
|
pos++
|
||||||
|
startPos++
|
||||||
|
for data[pos] != '`' {
|
||||||
|
pos++
|
||||||
|
if pos >= size {
|
||||||
|
ErrorLog("Unexpected end of text")
|
||||||
|
return string(data[startPos:size]), false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str := string(data[startPos:pos])
|
||||||
|
pos++
|
||||||
|
return str, true
|
||||||
|
|
||||||
|
} else if data[pos] == '\'' || data[pos] == '"' {
|
||||||
|
|
||||||
|
stopSymbol := data[pos]
|
||||||
|
pos++
|
||||||
|
startPos++
|
||||||
|
slash := false
|
||||||
|
for stopSymbol != data[pos] {
|
||||||
|
if data[pos] == '\\' {
|
||||||
|
pos += 2
|
||||||
|
slash = true
|
||||||
|
} else {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
if pos >= size {
|
||||||
|
ErrorLog("Unexpected end of text")
|
||||||
|
return string(data[startPos:size]), false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !slash {
|
||||||
|
str := string(data[startPos:pos])
|
||||||
|
pos++
|
||||||
|
skipSpaces(false)
|
||||||
|
return str, true
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := make([]rune, pos-startPos+1)
|
||||||
|
n1 := 0
|
||||||
|
n2 := startPos
|
||||||
|
|
||||||
|
invalidEscape := func() (string, bool) {
|
||||||
|
str := string(data[startPos:pos])
|
||||||
|
pos++
|
||||||
|
ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos)
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for n2 < pos {
|
||||||
|
if data[n2] != '\\' {
|
||||||
|
buffer[n1] = data[n2]
|
||||||
|
n2++
|
||||||
|
} else {
|
||||||
|
n2 += 2
|
||||||
|
switch data[n2-1] {
|
||||||
|
case 'n':
|
||||||
|
buffer[n1] = '\n'
|
||||||
|
|
||||||
|
case 'r':
|
||||||
|
buffer[n1] = '\r'
|
||||||
|
|
||||||
|
case 't':
|
||||||
|
buffer[n1] = '\t'
|
||||||
|
|
||||||
|
case '"':
|
||||||
|
buffer[n1] = '"'
|
||||||
|
|
||||||
|
case '\'':
|
||||||
|
buffer[n1] = '\''
|
||||||
|
|
||||||
|
case '\\':
|
||||||
|
buffer[n1] = '\\'
|
||||||
|
|
||||||
|
case 'x', 'X':
|
||||||
|
if n2+2 > pos {
|
||||||
|
return invalidEscape()
|
||||||
|
}
|
||||||
|
x := 0
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
ch := data[n2]
|
||||||
|
if ch >= '0' && ch <= '9' {
|
||||||
|
x = x*16 + int(ch-'0')
|
||||||
|
} else if ch >= 'a' && ch <= 'f' {
|
||||||
|
x = x*16 + int(ch-'a'+10)
|
||||||
|
} else if ch >= 'A' && ch <= 'F' {
|
||||||
|
x = x*16 + int(ch-'A'+10)
|
||||||
|
} else {
|
||||||
|
return invalidEscape()
|
||||||
|
}
|
||||||
|
n2++
|
||||||
|
}
|
||||||
|
buffer[n1] = rune(x)
|
||||||
|
|
||||||
|
case 'u', 'U':
|
||||||
|
if n2+4 > pos {
|
||||||
|
return invalidEscape()
|
||||||
|
}
|
||||||
|
x := 0
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
ch := data[n2]
|
||||||
|
if ch >= '0' && ch <= '9' {
|
||||||
|
x = x*16 + int(ch-'0')
|
||||||
|
} else if ch >= 'a' && ch <= 'f' {
|
||||||
|
x = x*16 + int(ch-'a'+10)
|
||||||
|
} else if ch >= 'A' && ch <= 'F' {
|
||||||
|
x = x*16 + int(ch-'A'+10)
|
||||||
|
} else {
|
||||||
|
return invalidEscape()
|
||||||
|
}
|
||||||
|
n2++
|
||||||
|
}
|
||||||
|
buffer[n1] = rune(x)
|
||||||
|
|
||||||
|
default:
|
||||||
|
str := string(data[startPos:pos])
|
||||||
|
ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos)
|
||||||
|
return str, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n1++
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++
|
||||||
|
skipSpaces(false)
|
||||||
|
return string(buffer[0:n1]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
stopSymbol := func(symbol rune) bool {
|
||||||
|
if unicode.IsSpace(symbol) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, sym := range []rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'} {
|
||||||
|
if sym == symbol {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for pos < size && !stopSymbol(data[pos]) {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
|
||||||
|
endPos := pos
|
||||||
|
skipSpaces(false)
|
||||||
|
if startPos == endPos {
|
||||||
|
ErrorLog("empty tag")
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return string(data[startPos:endPos]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
var parseObject func(tag string) DataObject
|
||||||
|
var parseArray func() []DataValue
|
||||||
|
|
||||||
|
parseNode := func() DataNode {
|
||||||
|
var tag string
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if tag, ok = parseTag(); !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
skipSpaces(true)
|
||||||
|
if data[pos] != '=' {
|
||||||
|
ErrorLogF("expected '=' after a tag name (line: %d, position: %d)", line, pos-lineStart)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++
|
||||||
|
skipSpaces(true)
|
||||||
|
switch data[pos] {
|
||||||
|
case '[':
|
||||||
|
node := new(dataNode)
|
||||||
|
node.tag = tag
|
||||||
|
|
||||||
|
if node.array = parseArray(); node.array == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
|
||||||
|
case '{':
|
||||||
|
node := new(dataNode)
|
||||||
|
node.tag = tag
|
||||||
|
if node.value = parseObject("_"); node.value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
|
||||||
|
case '}', ']', '=':
|
||||||
|
ErrorLogF("Expected '[', '{' or a tag name after '=' (line: %d, position: %d)", line, pos-lineStart)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
var str string
|
||||||
|
if str, ok = parseTag(); !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
node := new(dataNode)
|
||||||
|
node.tag = tag
|
||||||
|
|
||||||
|
if data[pos] == '{' {
|
||||||
|
if node.value = parseObject(str); node.value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val := new(dataStringValue)
|
||||||
|
val.value = str
|
||||||
|
node.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseObject = func(tag string) DataObject {
|
||||||
|
if data[pos] != '{' {
|
||||||
|
ErrorLogF("Expected '{' (line: %d, position: %d)", line, pos-lineStart)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
|
||||||
|
obj := new(dataObject)
|
||||||
|
obj.tag = tag
|
||||||
|
obj.property = []DataNode{}
|
||||||
|
|
||||||
|
for pos < size {
|
||||||
|
var node DataNode
|
||||||
|
|
||||||
|
skipSpaces(true)
|
||||||
|
if data[pos] == '}' {
|
||||||
|
pos++
|
||||||
|
skipSpaces(false)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
if node = parseNode(); node == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
obj.property = append(obj.property, node)
|
||||||
|
if data[pos] == '}' {
|
||||||
|
pos++
|
||||||
|
skipSpaces(true)
|
||||||
|
return obj
|
||||||
|
} else if data[pos] != ',' && data[pos] != '\n' {
|
||||||
|
ErrorLogF(`Expected '}', '\n' or ',' (line: %d, position: %d)`, line, pos-lineStart)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if data[pos] != '\n' {
|
||||||
|
pos++
|
||||||
|
}
|
||||||
|
skipSpaces(true)
|
||||||
|
for data[pos] == ',' {
|
||||||
|
pos++
|
||||||
|
skipSpaces(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog("Unexpected end of text")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parseArray = func() []DataValue {
|
||||||
|
pos++
|
||||||
|
skipSpaces(true)
|
||||||
|
|
||||||
|
array := []DataValue{}
|
||||||
|
|
||||||
|
for pos < size {
|
||||||
|
var tag string
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
skipSpaces(true)
|
||||||
|
for data[pos] == ',' && pos < size {
|
||||||
|
pos++
|
||||||
|
skipSpaces(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos >= size {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[pos] == ']' {
|
||||||
|
pos++
|
||||||
|
skipSpaces(true)
|
||||||
|
return array
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag, ok = parseTag(); !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[pos] == '{' {
|
||||||
|
obj := parseObject(tag)
|
||||||
|
if obj == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
array = append(array, obj)
|
||||||
|
} else {
|
||||||
|
val := new(dataStringValue)
|
||||||
|
val.value = tag
|
||||||
|
array = append(array, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch data[pos] {
|
||||||
|
case ']', ',', '\n':
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if data[pos] == ']' {
|
||||||
|
pos++
|
||||||
|
skipSpaces()
|
||||||
|
return array, nil
|
||||||
|
} else if data[pos] != ',' {
|
||||||
|
return nil, fmt.Errorf("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart)
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
skipSpaces()
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog("Unexpected end of text")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag, ok := parseTag(); ok {
|
||||||
|
return parseObject(tag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
/*
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDataWriter(t *testing.T) {
|
||||||
|
w := NewDataWriter()
|
||||||
|
w.StartObject("root")
|
||||||
|
w.WriteStringKey("key1", "text")
|
||||||
|
w.WriteStringKey("key2", "text 2")
|
||||||
|
w.WriteStringKey("key 3", "text4")
|
||||||
|
w.WriteStringsKey("key4", []string{"text4.1", "text4.2", "text4.3"}, '|')
|
||||||
|
w.WriteStringsKey("key5", []string{"text5.1", "text5.2", "text5.3"}, ',')
|
||||||
|
w.WriteColorKey("color", Color(0x7FD18243))
|
||||||
|
w.WriteColorsKey("colors", []Color{Color(0x7FD18243), Color(0xFF817263)}, ',')
|
||||||
|
w.WriteIntKey("int", 43)
|
||||||
|
w.WriteIntsKey("ints", []int{111, 222, 333}, '|')
|
||||||
|
|
||||||
|
w.StartObjectKey("obj", "xxx")
|
||||||
|
w.WriteSizeUnitKey("size", Px(16))
|
||||||
|
w.WriteSizeUnitsKey("sizes", []SizeUnit{Px(8), Percent(100)}, ',')
|
||||||
|
w.StartArray("array")
|
||||||
|
w.WriteStringToArray("text")
|
||||||
|
w.WriteColorToArray(Color(0x23456789))
|
||||||
|
w.WriteIntToArray(1)
|
||||||
|
w.WriteSizeUnitToArray(Inch(2))
|
||||||
|
w.FinishArray()
|
||||||
|
w.WriteBoundsKey("bounds1", Bounds{Px(8), Px(8), Px(8), Px(8)})
|
||||||
|
w.WriteBoundsKey("bounds2", Bounds{Px(8), Pt(12), Mm(4.5), Inch(1.2)})
|
||||||
|
w.FinishObject() // xxx
|
||||||
|
|
||||||
|
w.FinishObject() // root
|
||||||
|
|
||||||
|
text := w.String()
|
||||||
|
expected := `root {
|
||||||
|
key1 = text,
|
||||||
|
key2 = "text 2",
|
||||||
|
"key 3" = text4,
|
||||||
|
key4 = text4.1|text4.2|text4.3,
|
||||||
|
key5 = "text5.1,text5.2,text5.3",
|
||||||
|
color = #7FD18243,
|
||||||
|
colors = "#7FD18243,#FF817263",
|
||||||
|
int = 43,
|
||||||
|
ints = 111|222|333,
|
||||||
|
obj = xxx {
|
||||||
|
size = 16px,
|
||||||
|
sizes = "8px,100%",
|
||||||
|
array = [
|
||||||
|
text,
|
||||||
|
#23456789,
|
||||||
|
1,
|
||||||
|
2in
|
||||||
|
],
|
||||||
|
bounds1 = 8px,
|
||||||
|
bounds2 = "8px,12pt,4.5mm,1.2in"
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
if text != expected {
|
||||||
|
t.Error("DataWriter test fail. Result:\n`" + text + "`\nExpected:\n`" + expected + "`")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,211 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseDataText(t *testing.T) {
|
||||||
|
|
||||||
|
SetErrorLog(func(text string) {
|
||||||
|
t.Error(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
text := `obj1 {
|
||||||
|
key1 = val1,
|
||||||
|
key2=obj2{
|
||||||
|
key2.1=[val2.1,obj2.2{}, obj2.3{}],
|
||||||
|
"key 2.2"='val 2.2'
|
||||||
|
// Comment
|
||||||
|
key2.3/* comment */ = {
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Multiline comment
|
||||||
|
*/
|
||||||
|
'key2.4' = obj2.3{ text = " "},
|
||||||
|
key2.5= [],
|
||||||
|
},
|
||||||
|
key3 = "\n \t \\ \r \" ' \X4F\x4e \U01Ea",` +
|
||||||
|
"key4=`" + `\n \t \\ \r \" ' \x8F \UF80a` + "`\r}"
|
||||||
|
|
||||||
|
obj := ParseDataText(text)
|
||||||
|
if obj != nil {
|
||||||
|
if obj.Tag() != "obj1" {
|
||||||
|
t.Error(`obj.Tag() != "obj1"`)
|
||||||
|
}
|
||||||
|
if !obj.IsObject() {
|
||||||
|
t.Error(`!obj.IsObject()`)
|
||||||
|
}
|
||||||
|
if obj.PropertyCount() != 4 {
|
||||||
|
t.Error(`obj.PropertyCount() != 4`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Property(-1) != nil {
|
||||||
|
t.Error(`obj.Property(-1) != nil`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := obj.PropertyValue("key1"); !ok || val != "val1" {
|
||||||
|
t.Errorf(`obj.PropertyValue("key1") result: ("%s",%v)`, val, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := obj.PropertyValue("key3"); !ok || val != "\n \t \\ \r \" ' \x4F\x4e \u01Ea" {
|
||||||
|
t.Errorf(`obj.PropertyValue("key3") result: ("%s",%v)`, val, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := obj.PropertyValue("key4"); !ok || val != `\n \t \\ \r \" ' \x8F \UF80a` {
|
||||||
|
t.Errorf(`obj.PropertyValue("key4") result: ("%s",%v)`, val, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o := obj.PropertyObject("key2"); o == nil {
|
||||||
|
t.Error(`obj.PropertyObject("key2") == nil`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o := obj.PropertyObject("key1"); o != nil {
|
||||||
|
t.Error(`obj.PropertyObject("key1") != nil`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o := obj.PropertyObject("key5"); o != nil {
|
||||||
|
t.Error(`obj.PropertyObject("key5") != nil`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := obj.PropertyValue("key2"); ok {
|
||||||
|
t.Errorf(`obj.PropertyValue("key2") result: ("%s",%v)`, val, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := obj.PropertyValue("key5"); ok {
|
||||||
|
t.Errorf(`obj.PropertyValue("key5") result: ("%s",%v)`, val, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
testKey := func(obj DataObject, index int, tag string, nodeType int) DataNode {
|
||||||
|
key := obj.Property(index)
|
||||||
|
if key == nil {
|
||||||
|
t.Errorf(`%s.Property(%d) == nil`, obj.Tag(), index)
|
||||||
|
} else {
|
||||||
|
if key.Tag() != tag {
|
||||||
|
t.Errorf(`%s.Property(%d).Tag() != "%s"`, obj.Tag(), index, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if key.Type() != nodeType {
|
||||||
|
switch nodeType {
|
||||||
|
case TextNode:
|
||||||
|
t.Errorf(`%s.Property(%d) is not text`, obj.Tag(), index)
|
||||||
|
|
||||||
|
case ObjectNode:
|
||||||
|
t.Errorf(`%s.Property(%d) is not object`, obj.Tag(), index)
|
||||||
|
|
||||||
|
case ArrayNode:
|
||||||
|
t.Errorf(`%s.Property(%d) is not array`, obj.Tag(), index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
if key := testKey(obj, 0, "key1", TextNode); key != nil {
|
||||||
|
if key.Text() != "val1" {
|
||||||
|
t.Error(`key1.Value() != "val1"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if key := testKey(obj, 1, "key2", ObjectNode); key != nil {
|
||||||
|
o := key.Object()
|
||||||
|
if o == nil {
|
||||||
|
t.Error(`key2.Value().Object() == nil`)
|
||||||
|
} else {
|
||||||
|
if o.PropertyCount() != 5 {
|
||||||
|
t.Error(`key2.Value().Object().PropertyCount() != 4`)
|
||||||
|
}
|
||||||
|
|
||||||
|
type testKeyData struct {
|
||||||
|
tag string
|
||||||
|
nodeType int
|
||||||
|
}
|
||||||
|
|
||||||
|
data := []testKeyData{
|
||||||
|
{tag: "key2.1", nodeType: ArrayNode},
|
||||||
|
{tag: "key 2.2", nodeType: TextNode},
|
||||||
|
{tag: "key2.3", nodeType: ObjectNode},
|
||||||
|
{tag: "key2.4", nodeType: ObjectNode},
|
||||||
|
{tag: "key2.5", nodeType: ArrayNode},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, d := range data {
|
||||||
|
testKey(o, i, d.tag, d.nodeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
node1 := obj.Property(1)
|
||||||
|
if node1 == nil {
|
||||||
|
t.Error("obj.Property(1) != nil")
|
||||||
|
} else if node1.Type() != ObjectNode {
|
||||||
|
t.Error("obj.Property(1).Type() != ObjectNode")
|
||||||
|
} else if obj := node1.Object(); obj != nil {
|
||||||
|
if key := obj.Property(0); key != nil {
|
||||||
|
if key.Type() != ArrayNode {
|
||||||
|
t.Error("obj.Property(1).Object().Property(0)..Type() != ArrayNode")
|
||||||
|
} else {
|
||||||
|
if key.ArraySize() != 3 {
|
||||||
|
t.Error("obj.Property(1).Object().Property(0).ArraySize() != 3")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e := key.ArrayElement(0); e == nil {
|
||||||
|
t.Error("obj.Property(1).Object().Property(0).ArrayElement(0) == nil")
|
||||||
|
} else if e.IsObject() {
|
||||||
|
t.Error("obj.Property(1).Object().Property(0).ArrayElement(0).IsObject() == true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if e := key.ArrayElement(2); e == nil {
|
||||||
|
t.Error("obj.Property(1).Object().Property(0).ArrayElement(2) == nil")
|
||||||
|
} else if !e.IsObject() {
|
||||||
|
t.Error("obj.Property(1).Object().Property(0).ArrayElement(2).IsObject() == false")
|
||||||
|
} else if e.Value() != "" {
|
||||||
|
t.Error(`obj.Property(1).Object().Property(0).ArrayElement(2).Value() != ""`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e := key.ArrayElement(3); e != nil {
|
||||||
|
t.Error("obj.Property(1).Object().Property(0).ArrayElement(3) != nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Error("obj.Property(1).Object() == nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SetErrorLog(func(text string) {
|
||||||
|
})
|
||||||
|
|
||||||
|
failText := []string{
|
||||||
|
" ",
|
||||||
|
"obj[]",
|
||||||
|
"obj={}",
|
||||||
|
"obj{key}",
|
||||||
|
"obj{key=}",
|
||||||
|
"obj{key=val",
|
||||||
|
"obj{key=obj2{}",
|
||||||
|
"obj{key=obj2{key2}}",
|
||||||
|
"obj{key=\"val}",
|
||||||
|
"obj{key=val\"}",
|
||||||
|
"obj{key=\"val`}",
|
||||||
|
"obj{key=[}}",
|
||||||
|
"obj{key=[val",
|
||||||
|
"obj{key=[val,",
|
||||||
|
"obj{key=[obj2{]",
|
||||||
|
`obj{key="""}`,
|
||||||
|
`obj{key="\z"}`,
|
||||||
|
`obj{key="\xG6"}`,
|
||||||
|
`obj{key="\uG678"}`,
|
||||||
|
`obj{key="\x6"}`,
|
||||||
|
`obj{key="\u678"}`,
|
||||||
|
`obj{key1=val1 key2=val2}`,
|
||||||
|
`obj{key=//"\u678"}`,
|
||||||
|
`obj{key="\u678" /*}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, txt := range failText {
|
||||||
|
if obj := ParseDataText(txt); obj != nil {
|
||||||
|
t.Errorf("result ParseDataText(\"%s\") must be fail", txt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,404 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DateChangedEvent = "date-changed"
|
||||||
|
DatePickerMin = "date-picker-min"
|
||||||
|
DatePickerMax = "date-picker-max"
|
||||||
|
DatePickerStep = "date-picker-step"
|
||||||
|
DatePickerValue = "date-picker-value"
|
||||||
|
dateFormat = "2006-01-02"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DatePicker - DatePicker view
|
||||||
|
type DatePicker interface {
|
||||||
|
View
|
||||||
|
}
|
||||||
|
|
||||||
|
type datePickerData struct {
|
||||||
|
viewData
|
||||||
|
dateChangedListeners []func(DatePicker, time.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatePicker create new DatePicker object and return it
|
||||||
|
func NewDatePicker(session Session, params Params) DatePicker {
|
||||||
|
view := new(datePickerData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDatePicker(session Session) View {
|
||||||
|
return NewDatePicker(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) Init(session Session) {
|
||||||
|
picker.viewData.Init(session)
|
||||||
|
picker.tag = "DatePicker"
|
||||||
|
picker.dateChangedListeners = []func(DatePicker, time.Time){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case Type, Min, Max, Step, Value:
|
||||||
|
return "date-picker-" + tag
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) Remove(tag string) {
|
||||||
|
picker.remove(picker.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case DateChangedEvent:
|
||||||
|
if len(picker.dateChangedListeners) > 0 {
|
||||||
|
picker.dateChangedListeners = []func(DatePicker, time.Time){}
|
||||||
|
}
|
||||||
|
|
||||||
|
case DatePickerMin:
|
||||||
|
delete(picker.properties, DatePickerMin)
|
||||||
|
removeProperty(picker.htmlID(), Min, picker.session)
|
||||||
|
|
||||||
|
case DatePickerMax:
|
||||||
|
delete(picker.properties, DatePickerMax)
|
||||||
|
removeProperty(picker.htmlID(), Max, picker.session)
|
||||||
|
|
||||||
|
case DatePickerStep:
|
||||||
|
delete(picker.properties, DatePickerMax)
|
||||||
|
removeProperty(picker.htmlID(), Step, picker.session)
|
||||||
|
|
||||||
|
case DatePickerValue:
|
||||||
|
delete(picker.properties, DatePickerValue)
|
||||||
|
updateProperty(picker.htmlID(), Value, time.Now().Format(dateFormat), picker.session)
|
||||||
|
|
||||||
|
default:
|
||||||
|
picker.viewData.remove(tag)
|
||||||
|
picker.propertyChanged(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) Set(tag string, value interface{}) bool {
|
||||||
|
return picker.set(picker.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
picker.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeValue := func(tag string) (time.Time, bool) {
|
||||||
|
//old, oldOK := getDateProperty(picker, tag, shortTag)
|
||||||
|
switch value := value.(type) {
|
||||||
|
case time.Time:
|
||||||
|
picker.properties[tag] = value
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := picker.Session().resolveConstants(value); ok {
|
||||||
|
if date, err := time.Parse(dateFormat, text); err == nil {
|
||||||
|
picker.properties[tag] = value
|
||||||
|
return date, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case DatePickerMin:
|
||||||
|
old, oldOK := getDateProperty(picker, DatePickerMin, Min)
|
||||||
|
if date, ok := setTimeValue(DatePickerMin); ok {
|
||||||
|
if !oldOK || date != old {
|
||||||
|
updateProperty(picker.htmlID(), Min, date.Format(dateFormat), picker.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case DatePickerMax:
|
||||||
|
old, oldOK := getDateProperty(picker, DatePickerMax, Max)
|
||||||
|
if date, ok := setTimeValue(DatePickerMax); ok {
|
||||||
|
if !oldOK || date != old {
|
||||||
|
updateProperty(picker.htmlID(), Max, date.Format(dateFormat), picker.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case DatePickerStep:
|
||||||
|
oldStep := GetDatePickerStep(picker, "")
|
||||||
|
if picker.setIntProperty(DatePickerStep, value) {
|
||||||
|
step := GetDatePickerStep(picker, "")
|
||||||
|
if oldStep != step {
|
||||||
|
if step > 0 {
|
||||||
|
updateProperty(picker.htmlID(), Step, strconv.Itoa(step), picker.session)
|
||||||
|
} else {
|
||||||
|
removeProperty(picker.htmlID(), Step, picker.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case DatePickerValue:
|
||||||
|
oldDate := GetDatePickerValue(picker, "")
|
||||||
|
if date, ok := setTimeValue(DatePickerMax); ok {
|
||||||
|
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), date.Format(dateFormat)))
|
||||||
|
if date != oldDate {
|
||||||
|
for _, listener := range picker.dateChangedListeners {
|
||||||
|
listener(picker, date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case DateChangedEvent:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(DatePicker, time.Time):
|
||||||
|
picker.dateChangedListeners = []func(DatePicker, time.Time){value}
|
||||||
|
|
||||||
|
case func(time.Time):
|
||||||
|
fn := func(view DatePicker, date time.Time) {
|
||||||
|
value(date)
|
||||||
|
}
|
||||||
|
picker.dateChangedListeners = []func(DatePicker, time.Time){fn}
|
||||||
|
|
||||||
|
case []func(DatePicker, time.Time):
|
||||||
|
picker.dateChangedListeners = value
|
||||||
|
|
||||||
|
case []func(time.Time):
|
||||||
|
listeners := make([]func(DatePicker, time.Time), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[i] = func(view DatePicker, date time.Time) {
|
||||||
|
val(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picker.dateChangedListeners = listeners
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
listeners := make([]func(DatePicker, time.Time), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := val.(type) {
|
||||||
|
case func(DatePicker, time.Time):
|
||||||
|
listeners[i] = val
|
||||||
|
|
||||||
|
case func(time.Time):
|
||||||
|
listeners[i] = func(view DatePicker, date time.Time) {
|
||||||
|
val(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picker.dateChangedListeners = listeners
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
|
if picker.viewData.set(tag, value) {
|
||||||
|
picker.propertyChanged(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) Get(tag string) interface{} {
|
||||||
|
return picker.get(picker.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) get(tag string) interface{} {
|
||||||
|
switch tag {
|
||||||
|
case DateChangedEvent:
|
||||||
|
return picker.dateChangedListeners
|
||||||
|
|
||||||
|
default:
|
||||||
|
return picker.viewData.get(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) htmlTag() string {
|
||||||
|
return "input"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
picker.viewData.htmlProperties(self, buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(` type="date"`)
|
||||||
|
|
||||||
|
if min, ok := getDateProperty(picker, DatePickerMin, Min); ok {
|
||||||
|
buffer.WriteString(` min="`)
|
||||||
|
buffer.WriteString(min.Format(dateFormat))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
if max, ok := getDateProperty(picker, DatePickerMax, Max); ok {
|
||||||
|
buffer.WriteString(` max="`)
|
||||||
|
buffer.WriteString(max.Format(dateFormat))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
if step, ok := intProperty(picker, DatePickerStep, picker.Session(), 0); ok && step > 0 {
|
||||||
|
buffer.WriteString(` step="`)
|
||||||
|
buffer.WriteString(strconv.Itoa(step))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(` value="`)
|
||||||
|
buffer.WriteString(GetDatePickerValue(picker, "").Format(dateFormat))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
|
||||||
|
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||||
|
if IsDisabled(self) {
|
||||||
|
buffer.WriteString(` disabled`)
|
||||||
|
}
|
||||||
|
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
switch command {
|
||||||
|
case "textChanged":
|
||||||
|
if text, ok := data.PropertyValue("text"); ok {
|
||||||
|
if value, err := time.Parse(dateFormat, text); err == nil {
|
||||||
|
oldValue := GetDatePickerValue(picker, "")
|
||||||
|
picker.properties[DatePickerValue] = value
|
||||||
|
if value != oldValue {
|
||||||
|
for _, listener := range picker.dateChangedListeners {
|
||||||
|
listener(picker, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return picker.viewData.handleCommand(self, command, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
|
||||||
|
valueToTime := func(value interface{}) (time.Time, bool) {
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case time.Time:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := view.Session().resolveConstants(value); ok {
|
||||||
|
if result, err := time.Parse(dateFormat, text); err == nil {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := valueToTime(view.getRaw(mainTag)); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := valueFromStyle(view, shortTag); ok {
|
||||||
|
if result, ok := valueToTime(value); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatePickerMin returns the min date of DatePicker subview and "true" as the second value if the min date is set,
|
||||||
|
// "false" as the second value otherwise.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetDatePickerMin(view View, subviewID string) (time.Time, bool) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
return getDateProperty(view, DatePickerMin, Min)
|
||||||
|
}
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatePickerMax returns the max date of DatePicker subview and "true" as the second value if the min date is set,
|
||||||
|
// "false" as the second value otherwise.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetDatePickerMax(view View, subviewID string) (time.Time, bool) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
return getDateProperty(view, DatePickerMax, Max)
|
||||||
|
}
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatePickerStep returns the date changing step in days of DatePicker subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetDatePickerStep(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if result, _ := intStyledProperty(view, DatePickerStep, 0); result >= 0 {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDatePickerValue returns the date of DatePicker subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetDatePickerValue(view View, subviewID string) time.Time {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
date, _ := getDateProperty(view, DatePickerValue, Value)
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview.
|
||||||
|
// If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetDateChangedListeners(view View, subviewID string) []func(DatePicker, time.Time) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(DateChangedEvent); value != nil {
|
||||||
|
if listeners, ok := value.([]func(DatePicker, time.Time)); ok {
|
||||||
|
return listeners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(DatePicker, time.Time){}
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
theme {
|
||||||
|
colors = _{
|
||||||
|
ruiTextColor = #FF000000,
|
||||||
|
ruiDisabledTextColor = #FF202020,
|
||||||
|
ruiBackgroundColor = #FFFFFFFF,
|
||||||
|
ruiButtonColor = #FFE0E0E0,
|
||||||
|
ruiButtonActiveColor = #FFC0C0C0,
|
||||||
|
ruiButtonTextColor = #FF000000,
|
||||||
|
ruiButtonDisabledColor = #FFE0E0E0,
|
||||||
|
ruiButtonDisabledTextColor = #FF202020,
|
||||||
|
ruiHighlightColor = #FF1A74E8,
|
||||||
|
ruiHighlightTextColor = #FFFFFFFF,
|
||||||
|
ruiSelectedColor = #FFE0E0E0,
|
||||||
|
ruiSelectedTextColor = #FF000000,
|
||||||
|
ruiPopupBackgroundColor = #FFFFFFFF,
|
||||||
|
ruiPopupTextColor = #FF000000,
|
||||||
|
ruiPopupTitleColor = #FF0000FF,
|
||||||
|
ruiPopupTitleTextColor = #FFFFFFFF,
|
||||||
|
|
||||||
|
ruiTabsBackgroundColor = #FFEEEEEE,
|
||||||
|
ruiInactiveTabColor = #FFD0D0D0,
|
||||||
|
ruiInactiveTabTextColor = #FF202020,
|
||||||
|
ruiActiveTabColor = #FFFFFFFF,
|
||||||
|
ruiActiveTabTextColor = #FF000000,
|
||||||
|
},
|
||||||
|
colors:dark = _{
|
||||||
|
ruiTextColor = #FFE0E0E0,
|
||||||
|
ruiDisabledTextColor = #FFA0A0A0,
|
||||||
|
ruiBackgroundColor = #FF080808,
|
||||||
|
ruiButtonColor = #FF404040,
|
||||||
|
ruiButtonTextColor = #FFE0E0E0,
|
||||||
|
ruiButtonDisabledColor = #FF404040,
|
||||||
|
ruiButtonDisabledTextColor = #FFA0A0A0,
|
||||||
|
ruiHighlightColor = #FF1A74E8,
|
||||||
|
ruiHighlightTextColor = #FFFFFFFF,
|
||||||
|
},
|
||||||
|
constants = _{
|
||||||
|
ruiButtonHorizontalPadding = 16px,
|
||||||
|
ruiButtonVerticalPadding = 8px,
|
||||||
|
ruiButtonMargin = 4px,
|
||||||
|
ruiButtonRadius = 4px,
|
||||||
|
ruiButtonHighlightDilation = 1.5px,
|
||||||
|
ruiButtonHighlightBlur = 2px,
|
||||||
|
ruiCheckboxGap = 12px,
|
||||||
|
ruiListItemHorizontalPadding = 12px,
|
||||||
|
ruiListItemVerticalPadding = 4px,
|
||||||
|
ruiPopupTitleHeight = 32px,
|
||||||
|
ruiPopupTitlePadding = 8px,
|
||||||
|
ruiPopupButtonGap = 4px,
|
||||||
|
ruiTabSpace = 2px,
|
||||||
|
ruiTabHeight = 32px,
|
||||||
|
ruiTabPadding = 2px,
|
||||||
|
},
|
||||||
|
constants:touch = _{
|
||||||
|
ruiButtonHorizontalPadding = 20px,
|
||||||
|
ruiButtonVerticalPadding = 16px
|
||||||
|
},
|
||||||
|
styles = [
|
||||||
|
ruiApp {
|
||||||
|
font-name = "Arial, Helvetica, sans-serif",
|
||||||
|
text-size = 12pt,
|
||||||
|
text-color = @ruiTextColor,
|
||||||
|
background-color = @ruiBackgroundColor,
|
||||||
|
},
|
||||||
|
ruiButton {
|
||||||
|
align = center,
|
||||||
|
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
|
||||||
|
margin = @ruiButtonMargin,
|
||||||
|
radius = @ruiButtonRadius,
|
||||||
|
background-color = @ruiButtonColor,
|
||||||
|
text-color = @ruiButtonTextColor,
|
||||||
|
border = _{width = 1px, style = solid, color = @ruiButtonTextColor}
|
||||||
|
},
|
||||||
|
ruiDisabledButton {
|
||||||
|
align = center,
|
||||||
|
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
|
||||||
|
margin = @ruiButtonMargin,
|
||||||
|
radius = @ruiButtonRadius,
|
||||||
|
background-color = @ruiButtonDisabledColor,
|
||||||
|
text-color = @ruiButtonDisabledTextColor,
|
||||||
|
border = _{width = 1px, style = solid, color = @ruiButtonDisabledTextColor}
|
||||||
|
},
|
||||||
|
ruiButton:hover {
|
||||||
|
text-color = @ruiTextColor,
|
||||||
|
background-color = @ruiBackgroundColor,
|
||||||
|
},
|
||||||
|
ruiButton:focus {
|
||||||
|
shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor },
|
||||||
|
},
|
||||||
|
ruiButton:active {
|
||||||
|
background-color = @ruiButtonActiveColor
|
||||||
|
},
|
||||||
|
ruiCheckbox {
|
||||||
|
radius = 2px,
|
||||||
|
padding = 1px,
|
||||||
|
margin = 2px,
|
||||||
|
},
|
||||||
|
ruiCheckbox:focus {
|
||||||
|
margin = 0,
|
||||||
|
border = _{style = solid, color = @ruiHighlightColor, width = 2px },
|
||||||
|
},
|
||||||
|
ruiListItem {
|
||||||
|
radius = 4px,
|
||||||
|
padding = "@ruiListItemVerticalPadding, @ruiListItemHorizontalPadding, @ruiListItemVerticalPadding, @ruiListItemHorizontalPadding",
|
||||||
|
},
|
||||||
|
ruiListItemSelected {
|
||||||
|
background-color=@ruiSelectedColor,
|
||||||
|
text-color=@ruiSelectedTextColor,
|
||||||
|
},
|
||||||
|
ruiListItemFocused {
|
||||||
|
background-color=@ruiHighlightColor,
|
||||||
|
text-color=@ruiHighlightTextColor,
|
||||||
|
},
|
||||||
|
ruiActiveTab {
|
||||||
|
background-color = @ruiActiveTabColor,
|
||||||
|
text-color = @ruiActiveTabTextColor,
|
||||||
|
padding-left = 8px,
|
||||||
|
padding-right = 8px,
|
||||||
|
},
|
||||||
|
ruiInactiveTab {
|
||||||
|
background-color = @ruiInactiveTabColor,
|
||||||
|
text-color = @ruiInactiveTabTextColor,
|
||||||
|
padding-left = 8px,
|
||||||
|
padding-right = 8px,
|
||||||
|
},
|
||||||
|
ruiActiveVerticalTab {
|
||||||
|
background-color = @ruiActiveTabColor,
|
||||||
|
text-color = @ruiActiveTabTextColor,
|
||||||
|
padding-top = 8px,
|
||||||
|
padding-bottom = 8px,
|
||||||
|
},
|
||||||
|
ruiInactiveVerticalTab {
|
||||||
|
background-color = @ruiInactiveTabColor,
|
||||||
|
text-color = @ruiInactiveTabTextColor,
|
||||||
|
padding-top = 8px,
|
||||||
|
padding-bottom = 8px,
|
||||||
|
},
|
||||||
|
ruiPopup {
|
||||||
|
background-color = @ruiPopupBackgroundColor,
|
||||||
|
text-color = @ruiPopupTextColor,
|
||||||
|
radius = 4px,
|
||||||
|
shadow = _{spread-radius=4px, blur=16px, color=#80808080},
|
||||||
|
}
|
||||||
|
ruiPopupTitle {
|
||||||
|
background-color = @ruiPopupTitleColor,
|
||||||
|
text-color = @ruiPopupTitleTextColor,
|
||||||
|
min-height = 24px,
|
||||||
|
}
|
||||||
|
ruiMessageText {
|
||||||
|
padding-left = 64px,
|
||||||
|
padding-right = 64px,
|
||||||
|
padding-top = 32px,
|
||||||
|
padding-bottom = 32px,
|
||||||
|
}
|
||||||
|
ruiPopupMenuItem {
|
||||||
|
padding-top = 4px,
|
||||||
|
padding-bottom = 4px,
|
||||||
|
padding-left = 8px,
|
||||||
|
padding-right = 8px,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Summary is the constant for the "summary" property tag.
|
||||||
|
// The contents of the "summary" property are used as the label for the disclosure widget.
|
||||||
|
Summary = "summary"
|
||||||
|
// Expanded is the constant for the "expanded" property tag.
|
||||||
|
// If the "expanded" boolean property is "true", then the content of view is visible.
|
||||||
|
// If the value is "false" then the content is collapsed.
|
||||||
|
Expanded = "expanded"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetailsView - collapsible container of View
|
||||||
|
type DetailsView interface {
|
||||||
|
ViewsContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type detailsViewData struct {
|
||||||
|
viewsContainerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDetailsView create new DetailsView object and return it
|
||||||
|
func NewDetailsView(session Session, params Params) DetailsView {
|
||||||
|
view := new(detailsViewData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDetailsView(session Session) View {
|
||||||
|
return NewDetailsView(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of DetailsView by default values
|
||||||
|
func (detailsView *detailsViewData) Init(session Session) {
|
||||||
|
detailsView.viewsContainerData.Init(session)
|
||||||
|
detailsView.tag = "DetailsView"
|
||||||
|
//detailsView.systemClass = "ruiDetailsView"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) Remove(tag string) {
|
||||||
|
detailsView.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) remove(tag string) {
|
||||||
|
if _, ok := detailsView.properties[tag]; ok {
|
||||||
|
switch tag {
|
||||||
|
case Summary:
|
||||||
|
delete(detailsView.properties, tag)
|
||||||
|
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
|
||||||
|
|
||||||
|
case Expanded:
|
||||||
|
delete(detailsView.properties, tag)
|
||||||
|
removeProperty(detailsView.htmlID(), "open", detailsView.Session())
|
||||||
|
|
||||||
|
default:
|
||||||
|
detailsView.viewsContainerData.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) Set(tag string, value interface{}) bool {
|
||||||
|
return detailsView.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) set(tag string, value interface{}) bool {
|
||||||
|
switch tag {
|
||||||
|
case Summary:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
detailsView.properties[Summary] = value
|
||||||
|
|
||||||
|
case View:
|
||||||
|
detailsView.properties[Summary] = value
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
if view := CreateViewFromObject(detailsView.Session(), value); view != nil {
|
||||||
|
detailsView.properties[Summary] = view
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
|
||||||
|
return true
|
||||||
|
|
||||||
|
case Expanded:
|
||||||
|
if detailsView.setBoolProperty(tag, value) {
|
||||||
|
if IsDetailsExpanded(detailsView, "") {
|
||||||
|
updateProperty(detailsView.htmlID(), "open", "", detailsView.Session())
|
||||||
|
} else {
|
||||||
|
removeProperty(detailsView.htmlID(), "open", detailsView.Session())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return detailsView.viewsContainerData.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) Get(tag string) interface{} {
|
||||||
|
return detailsView.get(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) get(tag string) interface{} {
|
||||||
|
return detailsView.viewsContainerData.get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) htmlTag() string {
|
||||||
|
return "details"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
detailsView.viewsContainerData.htmlProperties(self, buffer)
|
||||||
|
if IsDetailsExpanded(detailsView, "") {
|
||||||
|
buffer.WriteString(` open`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (detailsView *detailsViewData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
if value, ok := detailsView.properties[Summary]; ok {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
buffer.WriteString("<summary>")
|
||||||
|
buffer.WriteString(value)
|
||||||
|
buffer.WriteString("</summary>")
|
||||||
|
|
||||||
|
case View:
|
||||||
|
buffer.WriteString("<summary>")
|
||||||
|
viewHTML(value, buffer)
|
||||||
|
buffer.WriteString("</summary>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detailsView.viewsContainerData.htmlSubviews(self, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDetailsSummary returns a value of the Summary property of DetailsView.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetDetailsSummary(view View, subviewID string) View {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(Summary); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
return NewTextView(view.Session(), Params{Text: value})
|
||||||
|
|
||||||
|
case View:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDetailsExpanded returns a value of the Expanded property of DetailsView.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func IsDetailsExpanded(view View, subviewID string) bool {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := boolStyledProperty(view, Expanded); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,346 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DropDownEvent = "drop-down-event"
|
||||||
|
|
||||||
|
// DropDownList - the interface of a drop-down list view
|
||||||
|
type DropDownList interface {
|
||||||
|
View
|
||||||
|
getItems() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type dropDownListData struct {
|
||||||
|
viewData
|
||||||
|
items []string
|
||||||
|
dropDownListener []func(DropDownList, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDropDownList create new DropDownList object and return it
|
||||||
|
func NewDropDownList(session Session, params Params) DropDownList {
|
||||||
|
view := new(dropDownListData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDropDownList(session Session) View {
|
||||||
|
return NewDropDownList(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) Init(session Session) {
|
||||||
|
list.viewData.Init(session)
|
||||||
|
list.tag = "DropDownList"
|
||||||
|
list.items = []string{}
|
||||||
|
list.dropDownListener = []func(DropDownList, int){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) Remove(tag string) {
|
||||||
|
list.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case Items:
|
||||||
|
if len(list.items) > 0 {
|
||||||
|
list.items = []string{}
|
||||||
|
updateInnerHTML(list.htmlID(), list.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Current:
|
||||||
|
list.set(Current, 0)
|
||||||
|
|
||||||
|
case DropDownEvent:
|
||||||
|
if len(list.dropDownListener) > 0 {
|
||||||
|
list.dropDownListener = []func(DropDownList, int){}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
list.viewData.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) Set(tag string, value interface{}) bool {
|
||||||
|
return list.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) set(tag string, value interface{}) bool {
|
||||||
|
switch tag {
|
||||||
|
case Items:
|
||||||
|
return list.setItems(value)
|
||||||
|
|
||||||
|
case Current:
|
||||||
|
oldCurrent := GetDropDownCurrent(list, "")
|
||||||
|
if !list.setIntProperty(Current, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !list.session.ignoreViewUpdates() {
|
||||||
|
current := GetDropDownCurrent(list, "")
|
||||||
|
if oldCurrent != current {
|
||||||
|
list.session.runScript(fmt.Sprintf(`selectDropDownListItem('%s', %d)`, list.htmlID(), current))
|
||||||
|
list.onSelectedItemChanged(current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case DropDownEvent:
|
||||||
|
return list.setDropDownListener(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.viewData.set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) setItems(value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
list.items = []string{value}
|
||||||
|
|
||||||
|
case []string:
|
||||||
|
list.items = value
|
||||||
|
|
||||||
|
case []DataValue:
|
||||||
|
list.items = []string{}
|
||||||
|
for _, val := range value {
|
||||||
|
if !val.IsObject() {
|
||||||
|
list.items = append(list.items, val.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case []fmt.Stringer:
|
||||||
|
list.items = make([]string, len(value))
|
||||||
|
for i, str := range value {
|
||||||
|
list.items[i] = str.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
items := []string{}
|
||||||
|
for _, v := range value {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
items = append(items, val)
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
items = append(items, val.String())
|
||||||
|
|
||||||
|
case bool:
|
||||||
|
if val {
|
||||||
|
items = append(items, "true")
|
||||||
|
} else {
|
||||||
|
items = append(items, "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
case float32:
|
||||||
|
items = append(items, fmt.Sprintf("%g", float64(val)))
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
items = append(items, fmt.Sprintf("%g", val))
|
||||||
|
|
||||||
|
case rune:
|
||||||
|
items = append(items, string(val))
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(v); ok {
|
||||||
|
items = append(items, strconv.Itoa(n))
|
||||||
|
} else {
|
||||||
|
notCompatibleType(Items, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list.items = items
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(Items, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !list.session.ignoreViewUpdates() {
|
||||||
|
updateInnerHTML(list.htmlID(), list.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) setDropDownListener(value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(DropDownList, int):
|
||||||
|
list.dropDownListener = []func(DropDownList, int){value}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case func(int):
|
||||||
|
list.dropDownListener = []func(DropDownList, int){func(list DropDownList, index int) {
|
||||||
|
value(index)
|
||||||
|
}}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case []func(DropDownList, int):
|
||||||
|
list.dropDownListener = value
|
||||||
|
return true
|
||||||
|
|
||||||
|
case []func(int):
|
||||||
|
listeners := make([]func(DropDownList, int), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(DropDownEvent, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
listeners[i] = func(list DropDownList, index int) {
|
||||||
|
val(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.dropDownListener = listeners
|
||||||
|
return true
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
listeners := make([]func(DropDownList, int), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(DropDownEvent, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch val := val.(type) {
|
||||||
|
case func(DropDownList, int):
|
||||||
|
listeners[i] = val
|
||||||
|
|
||||||
|
case func(int):
|
||||||
|
listeners[i] = func(list DropDownList, index int) {
|
||||||
|
val(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(DropDownEvent, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
list.dropDownListener = listeners
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
notCompatibleType(DropDownEvent, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) Get(tag string) interface{} {
|
||||||
|
return list.get(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) get(tag string) interface{} {
|
||||||
|
switch tag {
|
||||||
|
case Items:
|
||||||
|
return list.items
|
||||||
|
|
||||||
|
case Current:
|
||||||
|
result, _ := intProperty(list, Current, list.session, 0)
|
||||||
|
return result
|
||||||
|
|
||||||
|
case DropDownEvent:
|
||||||
|
return list.dropDownListener
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.viewData.get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) getItems() []string {
|
||||||
|
return list.items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) htmlTag() string {
|
||||||
|
return "select"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
if list.items != nil {
|
||||||
|
current := GetDropDownCurrent(list, "")
|
||||||
|
notTranslate := GetNotTranslate(list, "")
|
||||||
|
for i, item := range list.items {
|
||||||
|
if i == current {
|
||||||
|
buffer.WriteString("<option selected>")
|
||||||
|
} else {
|
||||||
|
buffer.WriteString("<option>")
|
||||||
|
}
|
||||||
|
if !notTranslate {
|
||||||
|
item, _ = list.session.GetString(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(item)
|
||||||
|
buffer.WriteString("</option>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
list.viewData.htmlProperties(self, buffer)
|
||||||
|
buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||||
|
list.viewData.htmlDisabledProperties(self, buffer)
|
||||||
|
if IsDisabled(list) {
|
||||||
|
buffer.WriteString(`disabled`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) onSelectedItemChanged(number int) {
|
||||||
|
for _, listener := range list.dropDownListener {
|
||||||
|
listener(list, number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (list *dropDownListData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
switch command {
|
||||||
|
case "itemSelected":
|
||||||
|
if text, ok := data.PropertyValue("number"); ok {
|
||||||
|
if number, err := strconv.Atoi(text); err == nil {
|
||||||
|
if GetDropDownCurrent(list, "") != number && number >= 0 && number < len(list.items) {
|
||||||
|
list.properties[Current] = number
|
||||||
|
list.onSelectedItemChanged(number)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return list.viewData.handleCommand(self, command, data)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDropDownListeners(view View) []func(DropDownList, int) {
|
||||||
|
if value := view.Get(DropDownEvent); value != nil {
|
||||||
|
if listeners, ok := value.([]func(DropDownList, int)); ok {
|
||||||
|
return listeners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(DropDownList, int){}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func GetDropDownItems return the view items list
|
||||||
|
func GetDropDownItems(view View, subviewID string) []string {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if list, ok := view.(DropDownList); ok {
|
||||||
|
return list.getItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func GetDropDownCurrentItem return the number of the selected item
|
||||||
|
func GetDropDownCurrent(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
result, _ := intProperty(view, Current, view.Session(), 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
|
@ -0,0 +1,632 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EditTextChangedEvent is the constant for the "edit-text-changed" property tag.
|
||||||
|
EditTextChangedEvent = "edit-text-changed"
|
||||||
|
// EditViewType is the constant for the "edit-view-type" property tag.
|
||||||
|
EditViewType = "edit-view-type"
|
||||||
|
// EditViewPattern is the constant for the "edit-view-pattern" property tag.
|
||||||
|
EditViewPattern = "edit-view-pattern"
|
||||||
|
// Spellcheck is the constant for the "spellcheck" property tag.
|
||||||
|
Spellcheck = "spellcheck"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SingleLineText - single-line text type of EditView
|
||||||
|
SingleLineText = 0
|
||||||
|
// PasswordText - password type of EditView
|
||||||
|
PasswordText = 1
|
||||||
|
// EmailText - e-mail type of EditView. Allows to enter one email
|
||||||
|
EmailText = 2
|
||||||
|
// EmailsText - e-mail type of EditView. Allows to enter multiple emails separeted by comma
|
||||||
|
EmailsText = 3
|
||||||
|
// URLText - url type of EditView. Allows to enter one url
|
||||||
|
URLText = 4
|
||||||
|
// PhoneText - telephone type of EditView. Allows to enter one phone number
|
||||||
|
PhoneText = 5
|
||||||
|
// MultiLineText - multi-line text type of EditView
|
||||||
|
MultiLineText = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// EditView - grid-container of View
|
||||||
|
type EditView interface {
|
||||||
|
View
|
||||||
|
AppendText(text string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type editViewData struct {
|
||||||
|
viewData
|
||||||
|
textChangeListeners []func(EditView, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEditView create new EditView object and return it
|
||||||
|
func NewEditView(session Session, params Params) EditView {
|
||||||
|
view := new(editViewData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEditView(session Session) View {
|
||||||
|
return NewEditView(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) Init(session Session) {
|
||||||
|
edit.viewData.Init(session)
|
||||||
|
edit.textChangeListeners = []func(EditView, string){}
|
||||||
|
edit.tag = "EditView"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case Type, "edit-type":
|
||||||
|
return EditViewType
|
||||||
|
|
||||||
|
case Pattern, "edit-pattern":
|
||||||
|
return EditViewPattern
|
||||||
|
|
||||||
|
case "maxlength", "maxlen":
|
||||||
|
return MaxLength
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) Remove(tag string) {
|
||||||
|
edit.remove(edit.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) remove(tag string) {
|
||||||
|
if _, ok := edit.properties[tag]; ok {
|
||||||
|
switch tag {
|
||||||
|
case Hint:
|
||||||
|
delete(edit.properties, Hint)
|
||||||
|
removeProperty(edit.htmlID(), "placeholder", edit.session)
|
||||||
|
|
||||||
|
case MaxLength:
|
||||||
|
delete(edit.properties, MaxLength)
|
||||||
|
removeProperty(edit.htmlID(), "maxlength", edit.session)
|
||||||
|
|
||||||
|
case ReadOnly, Spellcheck:
|
||||||
|
delete(edit.properties, tag)
|
||||||
|
updateBoolProperty(edit.htmlID(), tag, false, edit.session)
|
||||||
|
|
||||||
|
case EditTextChangedEvent:
|
||||||
|
if len(edit.textChangeListeners) > 0 {
|
||||||
|
edit.textChangeListeners = []func(EditView, string){}
|
||||||
|
}
|
||||||
|
|
||||||
|
case Text:
|
||||||
|
oldText := GetText(edit, "")
|
||||||
|
delete(edit.properties, tag)
|
||||||
|
if oldText != "" {
|
||||||
|
edit.textChanged("")
|
||||||
|
edit.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, edit.htmlID(), ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
case EditViewPattern:
|
||||||
|
oldText := GetEditViewPattern(edit, "")
|
||||||
|
delete(edit.properties, tag)
|
||||||
|
if oldText != "" {
|
||||||
|
removeProperty(edit.htmlID(), Pattern, edit.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case EditViewType:
|
||||||
|
oldType := GetEditViewType(edit, "")
|
||||||
|
delete(edit.properties, tag)
|
||||||
|
if oldType != 0 {
|
||||||
|
updateInnerHTML(edit.parentHTMLID(), edit.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Wrap:
|
||||||
|
oldWrap := IsEditViewWrap(edit, "")
|
||||||
|
delete(edit.properties, tag)
|
||||||
|
if GetEditViewType(edit, "") == MultiLineText {
|
||||||
|
if wrap := IsEditViewWrap(edit, ""); wrap != oldWrap {
|
||||||
|
if wrap {
|
||||||
|
updateProperty(edit.htmlID(), "wrap", "soft", edit.session)
|
||||||
|
} else {
|
||||||
|
updateProperty(edit.htmlID(), "wrap", "off", edit.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
edit.viewData.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) Set(tag string, value interface{}) bool {
|
||||||
|
return edit.set(edit.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
edit.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Text:
|
||||||
|
oldText := GetText(edit, "")
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
edit.properties[Text] = text
|
||||||
|
if text = GetText(edit, ""); oldText != text {
|
||||||
|
edit.textChanged(text)
|
||||||
|
if GetEditViewType(edit, "") == MultiLineText {
|
||||||
|
updateInnerHTML(edit.htmlID(), edit.Session())
|
||||||
|
} else {
|
||||||
|
text = strings.ReplaceAll(text, `"`, `\"`)
|
||||||
|
text = strings.ReplaceAll(text, `'`, `\'`)
|
||||||
|
text = strings.ReplaceAll(text, "\n", `\n`)
|
||||||
|
text = strings.ReplaceAll(text, "\r", `\r`)
|
||||||
|
edit.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, edit.htmlID(), text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case Hint:
|
||||||
|
oldText := GetHint(edit, "")
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
edit.properties[Hint] = text
|
||||||
|
if text = GetHint(edit, ""); oldText != text {
|
||||||
|
if text != "" {
|
||||||
|
updateProperty(edit.htmlID(), "placeholder", text, edit.session)
|
||||||
|
} else {
|
||||||
|
removeProperty(edit.htmlID(), "placeholder", edit.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case MaxLength:
|
||||||
|
oldMaxLength := GetMaxLength(edit, "")
|
||||||
|
if edit.setIntProperty(MaxLength, value) {
|
||||||
|
if maxLength := GetMaxLength(edit, ""); maxLength != oldMaxLength {
|
||||||
|
if maxLength > 0 {
|
||||||
|
updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength), edit.session)
|
||||||
|
} else {
|
||||||
|
removeProperty(edit.htmlID(), "maxlength", edit.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case ReadOnly:
|
||||||
|
if edit.setBoolProperty(ReadOnly, value) {
|
||||||
|
if IsReadOnly(edit, "") {
|
||||||
|
updateProperty(edit.htmlID(), ReadOnly, "", edit.session)
|
||||||
|
} else {
|
||||||
|
removeProperty(edit.htmlID(), ReadOnly, edit.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case Spellcheck:
|
||||||
|
if edit.setBoolProperty(Spellcheck, value) {
|
||||||
|
updateBoolProperty(edit.htmlID(), Spellcheck, IsSpellcheck(edit, ""), edit.session)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case EditViewPattern:
|
||||||
|
oldText := GetEditViewPattern(edit, "")
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
edit.properties[Pattern] = text
|
||||||
|
if text = GetEditViewPattern(edit, ""); oldText != text {
|
||||||
|
if text != "" {
|
||||||
|
updateProperty(edit.htmlID(), Pattern, text, edit.session)
|
||||||
|
} else {
|
||||||
|
removeProperty(edit.htmlID(), Pattern, edit.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case EditViewType:
|
||||||
|
oldType := GetEditViewType(edit, "")
|
||||||
|
if edit.setEnumProperty(EditViewType, value, enumProperties[EditViewType].values) {
|
||||||
|
if GetEditViewType(edit, "") != oldType {
|
||||||
|
updateInnerHTML(edit.parentHTMLID(), edit.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case Wrap:
|
||||||
|
oldWrap := IsEditViewWrap(edit, "")
|
||||||
|
if edit.setBoolProperty(Wrap, value) {
|
||||||
|
if GetEditViewType(edit, "") == MultiLineText {
|
||||||
|
if wrap := IsEditViewWrap(edit, ""); wrap != oldWrap {
|
||||||
|
if wrap {
|
||||||
|
updateProperty(edit.htmlID(), "wrap", "soft", edit.session)
|
||||||
|
} else {
|
||||||
|
updateProperty(edit.htmlID(), "wrap", "off", edit.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case EditTextChangedEvent:
|
||||||
|
ok := edit.setChangeListeners(value)
|
||||||
|
if !ok {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return edit.viewData.set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) setChangeListeners(value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(EditView, string):
|
||||||
|
edit.textChangeListeners = []func(EditView, string){value}
|
||||||
|
|
||||||
|
case func(string):
|
||||||
|
fn := func(view EditView, text string) {
|
||||||
|
value(text)
|
||||||
|
}
|
||||||
|
edit.textChangeListeners = []func(EditView, string){fn}
|
||||||
|
|
||||||
|
case []func(EditView, string):
|
||||||
|
edit.textChangeListeners = value
|
||||||
|
|
||||||
|
case []func(string):
|
||||||
|
listeners := make([]func(EditView, string), len(value))
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view EditView, text string) {
|
||||||
|
v(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edit.textChangeListeners = listeners
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
listeners := make([]func(EditView, string), len(value))
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch v := v.(type) {
|
||||||
|
case func(EditView, string):
|
||||||
|
listeners[i] = v
|
||||||
|
|
||||||
|
case func(string):
|
||||||
|
listeners[i] = func(view EditView, text string) {
|
||||||
|
v(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edit.textChangeListeners = listeners
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) Get(tag string) interface{} {
|
||||||
|
return edit.get(edit.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) get(tag string) interface{} {
|
||||||
|
return edit.viewData.get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) AppendText(text string) {
|
||||||
|
if GetEditViewType(edit, "") == MultiLineText {
|
||||||
|
if value := edit.getRaw(Text); value != nil {
|
||||||
|
if textValue, ok := value.(string); ok {
|
||||||
|
textValue += text
|
||||||
|
edit.properties[Text] = textValue
|
||||||
|
|
||||||
|
text := strings.ReplaceAll(text, `"`, `\"`)
|
||||||
|
text = strings.ReplaceAll(text, `'`, `\'`)
|
||||||
|
text = strings.ReplaceAll(text, "\n", `\n`)
|
||||||
|
text = strings.ReplaceAll(text, "\r", `\r`)
|
||||||
|
|
||||||
|
edit.session.runScript(`appendToInnerHTML("` + edit.htmlID() + `", "` + text + `")`)
|
||||||
|
|
||||||
|
edit.textChanged(textValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edit.set(Text, text)
|
||||||
|
} else {
|
||||||
|
edit.set(Text, GetText(edit, "")+text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) textChanged(newText string) {
|
||||||
|
for _, listener := range edit.textChangeListeners {
|
||||||
|
listener(edit, newText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) htmlTag() string {
|
||||||
|
if GetEditViewType(edit, "") == MultiLineText {
|
||||||
|
return "textarea"
|
||||||
|
}
|
||||||
|
return "input"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
edit.viewData.htmlProperties(self, buffer)
|
||||||
|
|
||||||
|
writeSpellcheck := func() {
|
||||||
|
if spellcheck := IsSpellcheck(edit, ""); spellcheck {
|
||||||
|
buffer.WriteString(` spellcheck="true"`)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` spellcheck="false"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editType := GetEditViewType(edit, "")
|
||||||
|
switch editType {
|
||||||
|
case SingleLineText:
|
||||||
|
buffer.WriteString(` type="text" inputmode="text"`)
|
||||||
|
writeSpellcheck()
|
||||||
|
|
||||||
|
case PasswordText:
|
||||||
|
buffer.WriteString(` type="password" inputmode="text"`)
|
||||||
|
|
||||||
|
case EmailText:
|
||||||
|
buffer.WriteString(` type="email" inputmode="email"`)
|
||||||
|
|
||||||
|
case EmailsText:
|
||||||
|
buffer.WriteString(` type="email" inputmode="email" multiple`)
|
||||||
|
|
||||||
|
case URLText:
|
||||||
|
buffer.WriteString(` type="url" inputmode="url"`)
|
||||||
|
|
||||||
|
case PhoneText:
|
||||||
|
buffer.WriteString(` type="tel" inputmode="tel"`)
|
||||||
|
|
||||||
|
case MultiLineText:
|
||||||
|
if IsEditViewWrap(edit, "") {
|
||||||
|
buffer.WriteString(` wrap="soft"`)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` wrap="off"`)
|
||||||
|
}
|
||||||
|
writeSpellcheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsReadOnly(edit, "") {
|
||||||
|
buffer.WriteString(` readonly`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxLength := GetMaxLength(edit, ""); maxLength > 0 {
|
||||||
|
buffer.WriteString(` maxlength="`)
|
||||||
|
buffer.WriteString(strconv.Itoa(maxLength))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
if hint := GetHint(edit, ""); hint != "" {
|
||||||
|
buffer.WriteString(` placeholder="`)
|
||||||
|
buffer.WriteString(hint)
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
|
||||||
|
if pattern := GetEditViewPattern(edit, ""); pattern != "" {
|
||||||
|
buffer.WriteString(` pattern="`)
|
||||||
|
buffer.WriteString(pattern)
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
if editType != MultiLineText {
|
||||||
|
if text := GetText(edit, ""); text != "" {
|
||||||
|
buffer.WriteString(` value="`)
|
||||||
|
buffer.WriteString(text)
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||||
|
if IsDisabled(self) {
|
||||||
|
buffer.WriteString(` disabled`)
|
||||||
|
}
|
||||||
|
edit.viewData.htmlDisabledProperties(self, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
if GetEditViewType(edit, "") == MultiLineText {
|
||||||
|
text := strings.ReplaceAll(GetText(edit, ""), `"`, `\"`)
|
||||||
|
text = strings.ReplaceAll(text, "\n", `\n`)
|
||||||
|
text = strings.ReplaceAll(text, "\r", `\r`)
|
||||||
|
buffer.WriteString(strings.ReplaceAll(text, `'`, `\'`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (edit *editViewData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
switch command {
|
||||||
|
case "textChanged":
|
||||||
|
oldText := GetText(edit, "")
|
||||||
|
if text, ok := data.PropertyValue("text"); ok {
|
||||||
|
edit.properties[Text] = text
|
||||||
|
if text := GetText(edit, ""); text != oldText {
|
||||||
|
edit.textChanged(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return edit.viewData.handleCommand(self, command, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetText returns a text of the subview.
|
||||||
|
// If the second argument (subviewID) is "" then a text of the first argument (view) is returned.
|
||||||
|
func GetText(view View, subviewID string) string {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if text, ok := stringProperty(view, Text, view.Session()); ok {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHint returns a hint text of the subview.
|
||||||
|
// If the second argument (subviewID) is "" then a text of the first argument (view) is returned.
|
||||||
|
func GetHint(view View, subviewID string) string {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if text, ok := stringProperty(view, Hint, view.Session()); ok {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
if text, ok := valueFromStyle(view, Hint); ok {
|
||||||
|
if text, ok = view.Session().resolveConstants(text); ok {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMaxLength returns a maximal lenght of EditView. If a maximal lenght is not limited then 0 is returned
|
||||||
|
// If the second argument (subviewID) is "" then a value of the first argument (view) is returned.
|
||||||
|
func GetMaxLength(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := intStyledProperty(view, MaxLength, 0); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReadOnly returns the true if a EditView works in read only mode.
|
||||||
|
// If the second argument (subviewID) is "" then a value of the first argument (view) is returned.
|
||||||
|
func IsReadOnly(view View, subviewID string) bool {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := boolStyledProperty(view, ReadOnly); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSpellcheck returns a value of the Spellcheck property of EditView.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func IsSpellcheck(view View, subviewID string) bool {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if spellcheck, ok := boolStyledProperty(view, Spellcheck); ok {
|
||||||
|
return spellcheck
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview.
|
||||||
|
// If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTextChangedListeners(view View, subviewID string) []func(EditView, string) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(EditTextChangedEvent); value != nil {
|
||||||
|
if result, ok := value.([]func(EditView, string)); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(EditView, string){}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEditViewType returns a value of the Type property of EditView.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetEditViewType(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return SingleLineText
|
||||||
|
}
|
||||||
|
t, _ := enumStyledProperty(view, EditViewType, SingleLineText)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEditViewPattern returns a value of the Pattern property of EditView.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetEditViewPattern(view View, subviewID string) string {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok {
|
||||||
|
return pattern
|
||||||
|
}
|
||||||
|
if pattern, ok := valueFromStyle(view, EditViewPattern); ok {
|
||||||
|
if pattern, ok = view.Session().resolveConstants(pattern); ok {
|
||||||
|
return pattern
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEditViewWrap returns a value of the Wrap property of MultiLineEditView.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func IsEditViewWrap(view View, subviewID string) bool {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if wrap, ok := boolStyledProperty(view, Wrap); ok {
|
||||||
|
return wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendEditText appends the text to the EditView content.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func AppendEditText(view View, subviewID string, text string) {
|
||||||
|
if subviewID != "" {
|
||||||
|
if edit := EditViewByID(view, subviewID); edit != nil {
|
||||||
|
edit.AppendText(text)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if edit, ok := view.(EditView); ok {
|
||||||
|
edit.AppendText(text)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FocusEvent is the constant for "focus-event" property tag
|
||||||
|
// The "focus-event" event occurs when the View takes input focus.
|
||||||
|
// The main listener format: func(View).
|
||||||
|
// The additional listener format: func().
|
||||||
|
FocusEvent = "focus-event"
|
||||||
|
|
||||||
|
// LostFocusEvent is the constant for "lost-focus-event" property tag
|
||||||
|
// The "lost-focus-event" event occurs when the View lost input focus.
|
||||||
|
// The main listener format: func(View).
|
||||||
|
// The additional listener format: func().
|
||||||
|
LostFocusEvent = "lost-focus-event"
|
||||||
|
)
|
||||||
|
|
||||||
|
func valueToFocusListeners(value interface{}) ([]func(View), bool) {
|
||||||
|
if value == nil {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(View):
|
||||||
|
return []func(View){value}, true
|
||||||
|
|
||||||
|
case func():
|
||||||
|
fn := func(View) {
|
||||||
|
value()
|
||||||
|
}
|
||||||
|
return []func(View){fn}, true
|
||||||
|
|
||||||
|
case []func(View):
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
for _, fn := range value {
|
||||||
|
if fn == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case []func():
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(View) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
switch v := v.(type) {
|
||||||
|
case func(View):
|
||||||
|
listeners[i] = v
|
||||||
|
|
||||||
|
case func():
|
||||||
|
listeners[i] = func(View) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var focusEvents = map[string]struct{ jsEvent, jsFunc string }{
|
||||||
|
FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"},
|
||||||
|
LostFocusEvent: {jsEvent: "onblur", jsFunc: "blurEvent"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setFocusListener(tag string, value interface{}) bool {
|
||||||
|
listeners, ok := valueToFocusListeners(value)
|
||||||
|
if !ok {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if listeners == nil {
|
||||||
|
view.removeFocusListener(tag)
|
||||||
|
} else if js, ok := focusEvents[tag]; ok {
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
if view.created {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) removeFocusListener(tag string) {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
if view.created {
|
||||||
|
if js, ok := focusEvents[tag]; ok {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFocusListeners(view View, subviewID string, tag string) []func(View) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(tag); value != nil {
|
||||||
|
if result, ok := value.([]func(View)); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(View){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func focusEventsHtml(view View, buffer *strings.Builder) {
|
||||||
|
for tag, js := range focusEvents {
|
||||||
|
if value := view.getRaw(tag); value != nil {
|
||||||
|
if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 {
|
||||||
|
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetFocusListeners(view View, subviewID string) []func(View) {
|
||||||
|
return getFocusListeners(view, subviewID, FocusEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLostFocusListeners returns a LostFocusListener list. If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetLostFocusListeners(view View, subviewID string) []func(View) {
|
||||||
|
return getFocusListeners(view, subviewID, LostFocusEvent)
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
module github.com/anoshenko/rui
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require github.com/gorilla/websocket v1.4.2
|
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
@ -0,0 +1,391 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GridLayout - grid-container of View
|
||||||
|
type GridLayout interface {
|
||||||
|
ViewsContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type gridLayoutData struct {
|
||||||
|
viewsContainerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGridLayout create new GridLayout object and return it
|
||||||
|
func NewGridLayout(session Session, params Params) GridLayout {
|
||||||
|
view := new(gridLayoutData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGridLayout(session Session) View {
|
||||||
|
return NewGridLayout(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of GridLayout by default values
|
||||||
|
func (gridLayout *gridLayoutData) Init(session Session) {
|
||||||
|
gridLayout.viewsContainerData.Init(session)
|
||||||
|
gridLayout.tag = "GridLayout"
|
||||||
|
gridLayout.systemClass = "ruiGridLayout"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) setGridCellSize(tag string, value interface{}) bool {
|
||||||
|
setValues := func(values []string) bool {
|
||||||
|
count := len(values)
|
||||||
|
if count > 1 {
|
||||||
|
sizes := make([]interface{}, count)
|
||||||
|
for i, val := range values {
|
||||||
|
val = strings.Trim(val, " \t\n\r")
|
||||||
|
if isConstantName(val) {
|
||||||
|
sizes[i] = val
|
||||||
|
} else if size, ok := StringToSizeUnit(val); ok {
|
||||||
|
sizes[i] = size
|
||||||
|
} else {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style.properties[tag] = sizes
|
||||||
|
} else if isConstantName(values[0]) {
|
||||||
|
style.properties[tag] = values[0]
|
||||||
|
} else if size, ok := StringToSizeUnit(values[0]); ok {
|
||||||
|
style.properties[tag] = size
|
||||||
|
} else {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case CellWidth, CellHeight:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case SizeUnit, []SizeUnit:
|
||||||
|
style.properties[tag] = value
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if !setValues(strings.Split(value, ",")) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case []string:
|
||||||
|
if !setValues(value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case []DataValue:
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
values := make([]string, count)
|
||||||
|
for i, val := range value {
|
||||||
|
if val.IsObject() {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
values[i] = val.Value()
|
||||||
|
}
|
||||||
|
if !setValues(values) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sizes := make([]interface{}, count)
|
||||||
|
for i, val := range value {
|
||||||
|
switch val := val.(type) {
|
||||||
|
case SizeUnit:
|
||||||
|
sizes[i] = val
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if isConstantName(val) {
|
||||||
|
sizes[i] = val
|
||||||
|
} else if size, ok := StringToSizeUnit(val); ok {
|
||||||
|
sizes[i] = size
|
||||||
|
} else {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style.properties[tag] = sizes
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string {
|
||||||
|
switch cellSize := gridCellSizes(style, tag, session); len(cellSize) {
|
||||||
|
case 0:
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
if cellSize[0].Type != Auto {
|
||||||
|
return `repeat(auto-fill, ` + cellSize[0].cssString(`auto`) + `)`
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
allAuto := true
|
||||||
|
allEqual := true
|
||||||
|
for i, size := range cellSize {
|
||||||
|
if size.Type != Auto {
|
||||||
|
allAuto = false
|
||||||
|
}
|
||||||
|
if i > 0 && !size.Equal(cellSize[0]) {
|
||||||
|
allEqual = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !allAuto {
|
||||||
|
if allEqual {
|
||||||
|
return fmt.Sprintf(`repeat(%d, %s)`, len(cellSize), cellSize[0].cssString(`auto`))
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
for _, size := range cellSize {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(size.cssString(`auto`))
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gridLayout *gridLayoutData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case VerticalAlign:
|
||||||
|
return CellVerticalAlign
|
||||||
|
|
||||||
|
case HorizontalAlign:
|
||||||
|
return CellHorizontalAlign
|
||||||
|
|
||||||
|
case "row-gap":
|
||||||
|
return GridRowGap
|
||||||
|
|
||||||
|
case ColumnGap:
|
||||||
|
return GridColumnGap
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gridLayout *gridLayoutData) Get(tag string) interface{} {
|
||||||
|
return gridLayout.get(gridLayout.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gridLayout *gridLayoutData) get(tag string) interface{} {
|
||||||
|
if tag == Gap {
|
||||||
|
rowGap := GetGridRowGap(gridLayout, "")
|
||||||
|
columnGap := GetGridColumnGap(gridLayout, "")
|
||||||
|
if rowGap.Equal(columnGap) {
|
||||||
|
return rowGap
|
||||||
|
}
|
||||||
|
return AutoSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
return gridLayout.viewsContainerData.get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gridLayout *gridLayoutData) Remove(tag string) {
|
||||||
|
gridLayout.remove(gridLayout.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gridLayout *gridLayoutData) remove(tag string) {
|
||||||
|
if tag == Gap {
|
||||||
|
gridLayout.remove(GridRowGap)
|
||||||
|
gridLayout.remove(GridColumnGap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gridLayout.viewsContainerData.remove(tag)
|
||||||
|
switch tag {
|
||||||
|
case CellWidth:
|
||||||
|
updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
|
||||||
|
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
|
||||||
|
|
||||||
|
case CellHeight:
|
||||||
|
updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
|
||||||
|
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gridLayout *gridLayoutData) Set(tag string, value interface{}) bool {
|
||||||
|
return gridLayout.set(gridLayout.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gridLayout *gridLayoutData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
gridLayout.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag == Gap {
|
||||||
|
return gridLayout.set(GridRowGap, value) && gridLayout.set(GridColumnGap, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if gridLayout.viewsContainerData.set(tag, value) {
|
||||||
|
switch tag {
|
||||||
|
case CellWidth:
|
||||||
|
updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
|
||||||
|
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
|
||||||
|
|
||||||
|
case CellHeight:
|
||||||
|
updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
|
||||||
|
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
|
||||||
|
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func gridCellSizes(properties Properties, tag string, session Session) []SizeUnit {
|
||||||
|
if value := properties.Get(tag); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case []SizeUnit:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
return []SizeUnit{value}
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
result := make([]SizeUnit, len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
result[i] = AutoSize()
|
||||||
|
switch val := val.(type) {
|
||||||
|
case SizeUnit:
|
||||||
|
result[i] = val
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(val); ok {
|
||||||
|
result[i], _ = StringToSizeUnit(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
values := strings.Split(text, ",")
|
||||||
|
result := make([]SizeUnit, len(values))
|
||||||
|
for i, val := range values {
|
||||||
|
result[i], _ = StringToSizeUnit(val)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []SizeUnit{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gridLayout *gridLayoutData) cssStyle(self View, builder cssBuilder) {
|
||||||
|
gridLayout.viewsContainerData.cssStyle(self, builder)
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3)
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetCellVerticalAlign(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if align, ok := enumProperty(view, CellVerticalAlign, view.Session(), StretchAlign); ok {
|
||||||
|
return align
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StretchAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCellHorizontalAlign returns the vertical align of a GridLayout cell content: LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3)
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetCellHorizontalAlign(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if align, ok := enumProperty(view, CellHorizontalAlign, view.Session(), StretchAlign); ok {
|
||||||
|
return align
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StretchAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCellWidth returns the width of a GridLayout cell. If the result is an empty array, then the width is not set.
|
||||||
|
// If the result is a single value array, then the width of all cell is equal.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetCellWidth(view View, subviewID string) []SizeUnit {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
return gridCellSizes(view, CellWidth, view.Session())
|
||||||
|
}
|
||||||
|
return []SizeUnit{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCellHeight returns the height of a GridLayout cell. If the result is an empty array, then the height is not set.
|
||||||
|
// If the result is a single value array, then the height of all cell is equal.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetCellHeight(view View, subviewID string) []SizeUnit {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
return gridCellSizes(view, CellHeight, view.Session())
|
||||||
|
}
|
||||||
|
return []SizeUnit{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGridRowGap returns the gap between GridLayout rows.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetGridRowGap(view View, subviewID string) SizeUnit {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := sizeProperty(view, GridRowGap, view.Session()); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AutoSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetGridColumnGap returns the gap between GridLayout columns.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetGridColumnGap(view View, subviewID string) SizeUnit {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := sizeProperty(view, GridColumnGap, view.Session()); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AutoSize()
|
||||||
|
}
|
|
@ -0,0 +1,132 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ImageLoading is the image loading status: in the process of loading
|
||||||
|
ImageLoading = 0
|
||||||
|
// ImageReady is the image loading status: the image is loaded successfully
|
||||||
|
ImageReady = 1
|
||||||
|
// ImageLoadingError is the image loading status: an error occurred while loading
|
||||||
|
ImageLoadingError = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Image defines the image that is used for drawing operations on the Canvas.
|
||||||
|
type Image interface {
|
||||||
|
// URL returns the url of the image
|
||||||
|
URL() string
|
||||||
|
// LoadingStatus returns the status of the image loading: ImageLoading (0), ImageReady (1), ImageLoadingError (2)
|
||||||
|
LoadingStatus() int
|
||||||
|
// LoadingError: if LoadingStatus() == ImageLoadingError then returns the error text, "" otherwise
|
||||||
|
LoadingError() string
|
||||||
|
setLoadingError(err string)
|
||||||
|
// Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0
|
||||||
|
Width() float64
|
||||||
|
// Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0
|
||||||
|
Height() float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageData struct {
|
||||||
|
url string
|
||||||
|
loadingStatus int
|
||||||
|
loadingError string
|
||||||
|
width, height float64
|
||||||
|
listener func(Image)
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageManager struct {
|
||||||
|
images map[string]*imageData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *imageData) URL() string {
|
||||||
|
return image.url
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *imageData) LoadingStatus() int {
|
||||||
|
return image.loadingStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *imageData) LoadingError() string {
|
||||||
|
return image.loadingError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *imageData) setLoadingError(err string) {
|
||||||
|
image.loadingError = err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *imageData) Width() float64 {
|
||||||
|
return image.width
|
||||||
|
}
|
||||||
|
|
||||||
|
func (image *imageData) Height() float64 {
|
||||||
|
return image.height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *imageManager) loadImage(url string, onLoaded func(Image), session Session) Image {
|
||||||
|
if manager.images == nil {
|
||||||
|
manager.images = make(map[string]*imageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if image, ok := manager.images[url]; ok && image.loadingStatus == ImageReady {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
image := new(imageData)
|
||||||
|
image.url = url
|
||||||
|
image.listener = onLoaded
|
||||||
|
image.loadingStatus = ImageLoading
|
||||||
|
manager.images[url] = image
|
||||||
|
session.runScript("loadImage('" + url + "');")
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
|
||||||
|
if manager.images == nil {
|
||||||
|
manager.images = make(map[string]*imageData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if url, ok := obj.PropertyValue("url"); ok {
|
||||||
|
if image, ok := manager.images[url]; ok {
|
||||||
|
image.loadingStatus = ImageReady
|
||||||
|
if width, ok := obj.PropertyValue("width"); ok {
|
||||||
|
if w, err := strconv.ParseFloat(width, 64); err == nil {
|
||||||
|
image.width = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if height, ok := obj.PropertyValue("height"); ok {
|
||||||
|
if h, err := strconv.ParseFloat(height, 64); err == nil {
|
||||||
|
image.height = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if image.listener != nil {
|
||||||
|
image.listener(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *imageManager) imageLoadError(obj DataObject, session Session) {
|
||||||
|
if manager.images == nil {
|
||||||
|
manager.images = make(map[string]*imageData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if url, ok := obj.PropertyValue("url"); ok {
|
||||||
|
if image, ok := manager.images[url]; ok {
|
||||||
|
delete(manager.images, url)
|
||||||
|
|
||||||
|
text, _ := obj.PropertyValue("message")
|
||||||
|
image.setLoadingError(text)
|
||||||
|
|
||||||
|
if image.listener != nil {
|
||||||
|
image.listener(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadImage starts the async image loading by url
|
||||||
|
func LoadImage(url string, onLoaded func(Image), session Session) Image {
|
||||||
|
return session.imageManager().loadImage(url, onLoaded, session)
|
||||||
|
}
|
|
@ -0,0 +1,264 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NoneFit - value of the "object-fit" property of an ImageView. The replaced content is not resized
|
||||||
|
NoneFit = 0
|
||||||
|
// ContainFit - value of the "object-fit" property of an ImageView. The replaced content
|
||||||
|
// is scaled to maintain its aspect ratio while fitting within the element’s content box.
|
||||||
|
// The entire object is made to fill the box, while preserving its aspect ratio, so the object
|
||||||
|
// will be "letterboxed" if its aspect ratio does not match the aspect ratio of the box.
|
||||||
|
ContainFit = 1
|
||||||
|
// CoverFit - value of the "object-fit" property of an ImageView. The replaced content
|
||||||
|
// is sized to maintain its aspect ratio while filling the element’s entire content box.
|
||||||
|
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
|
||||||
|
CoverFit = 2
|
||||||
|
// FillFit - value of the "object-fit" property of an ImageView. The replaced content is sized
|
||||||
|
// to fill the element’s content box. The entire object will completely fill the box.
|
||||||
|
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be stretched to fit.
|
||||||
|
FillFit = 3
|
||||||
|
// ScaleDownFit - value of the "object-fit" property of an ImageView. The content is sized as
|
||||||
|
// if NoneFit or ContainFit were specified, whichever would result in a smaller concrete object size.
|
||||||
|
ScaleDownFit = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// ImageView - image View
|
||||||
|
type ImageView interface {
|
||||||
|
View
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageViewData struct {
|
||||||
|
viewData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewImageView create new ImageView object and return it
|
||||||
|
func NewImageView(session Session, params Params) ImageView {
|
||||||
|
view := new(imageViewData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newImageView(session Session) View {
|
||||||
|
return NewImageView(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of imageView by default values
|
||||||
|
func (imageView *imageViewData) Init(session Session) {
|
||||||
|
imageView.viewData.Init(session)
|
||||||
|
imageView.tag = "ImageView"
|
||||||
|
//imageView.systemClass = "ruiImageView"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case "source":
|
||||||
|
tag = Source
|
||||||
|
|
||||||
|
case VerticalAlign:
|
||||||
|
tag = ImageVerticalAlign
|
||||||
|
|
||||||
|
case HorizontalAlign:
|
||||||
|
tag = ImageHorizontalAlign
|
||||||
|
|
||||||
|
case altProperty:
|
||||||
|
tag = AltText
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) Remove(tag string) {
|
||||||
|
imageView.remove(imageView.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) remove(tag string) {
|
||||||
|
imageView.viewData.remove(tag)
|
||||||
|
switch tag {
|
||||||
|
case Source:
|
||||||
|
updateProperty(imageView.htmlID(), "src", "", imageView.session)
|
||||||
|
removeProperty(imageView.htmlID(), "srcset", imageView.session)
|
||||||
|
|
||||||
|
case AltText:
|
||||||
|
updateInnerHTML(imageView.htmlID(), imageView.session)
|
||||||
|
|
||||||
|
case ImageVerticalAlign, ImageHorizontalAlign:
|
||||||
|
updateCSSStyle(imageView.htmlID(), imageView.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) Set(tag string, value interface{}) bool {
|
||||||
|
return imageView.set(imageView.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
imageView.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Source:
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
imageView.properties[Source] = text
|
||||||
|
updateProperty(imageView.htmlID(), "src", text, imageView.session)
|
||||||
|
if srcset := imageView.srcSet(text); srcset != "" {
|
||||||
|
updateProperty(imageView.htmlID(), "srcset", srcset, imageView.session)
|
||||||
|
} else {
|
||||||
|
removeProperty(imageView.htmlID(), "srcset", imageView.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
|
||||||
|
case AltText:
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
imageView.properties[AltText] = text
|
||||||
|
updateInnerHTML(imageView.htmlID(), imageView.session)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
if imageView.viewData.set(tag, value) {
|
||||||
|
switch tag {
|
||||||
|
case ImageVerticalAlign, ImageHorizontalAlign:
|
||||||
|
updateCSSStyle(imageView.htmlID(), imageView.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) Get(tag string) interface{} {
|
||||||
|
return imageView.viewData.get(imageView.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) srcSet(path string) string {
|
||||||
|
if srcset, ok := resources.imageSrcSets[path]; ok {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
for i, src := range srcset {
|
||||||
|
if i > 0 {
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
}
|
||||||
|
buffer.WriteString(src.path)
|
||||||
|
buffer.WriteString(fmt.Sprintf(" %gx", src.scale))
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) htmlTag() string {
|
||||||
|
return "img"
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func (imageView *imageViewData) closeHTMLTag() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
imageView.viewData.htmlProperties(self, buffer)
|
||||||
|
imageResource := GetImageViewSource(imageView, "")
|
||||||
|
if imageResource != "" {
|
||||||
|
buffer.WriteString(` src="`)
|
||||||
|
buffer.WriteString(imageResource)
|
||||||
|
buffer.WriteString(`"`)
|
||||||
|
if srcset := imageView.srcSet(imageResource); srcset != "" {
|
||||||
|
buffer.WriteString(` srcset="`)
|
||||||
|
buffer.WriteString(srcset)
|
||||||
|
buffer.WriteString(`"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (imageView *imageViewData) cssStyle(self View, builder cssBuilder) {
|
||||||
|
imageView.viewData.cssStyle(self, builder)
|
||||||
|
|
||||||
|
if value, ok := enumProperty(imageView, Fit, imageView.session, 0); ok {
|
||||||
|
builder.add("object-fit", enumProperties[Fit].cssValues[value])
|
||||||
|
} else {
|
||||||
|
builder.add("object-fit", "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
vAlign := GetImageViewVerticalAlign(imageView, "")
|
||||||
|
hAlign := GetImageViewHorizontalAlign(imageView, "")
|
||||||
|
if vAlign != CenterAlign || hAlign != CenterAlign {
|
||||||
|
var position string
|
||||||
|
switch hAlign {
|
||||||
|
case LeftAlign:
|
||||||
|
position = "left"
|
||||||
|
case RightAlign:
|
||||||
|
position = "right"
|
||||||
|
default:
|
||||||
|
position = "center"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch vAlign {
|
||||||
|
case TopAlign:
|
||||||
|
position += " top"
|
||||||
|
case BottomAlign:
|
||||||
|
position += " bottom"
|
||||||
|
default:
|
||||||
|
position += " center"
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add("object-position", position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImageViewSource returns the image URL of an ImageView subview.
|
||||||
|
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
|
||||||
|
func GetImageViewSource(view View, subviewID string) string {
|
||||||
|
if image, ok := stringProperty(view, Source, view.Session()); ok {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImageViewAltText returns an alternative text description of an ImageView subview.
|
||||||
|
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
|
||||||
|
func GetImageViewAltText(view View, subviewID string) string {
|
||||||
|
if text, ok := stringProperty(view, AltText, view.Session()); ok {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImageViewFit returns how the content of a replaced ImageView subview:
|
||||||
|
// NoneFit (0), ContainFit (1), CoverFit (2), FillFit (3), or ScaleDownFit (4).
|
||||||
|
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
|
||||||
|
func GetImageViewFit(view View, subviewID string) int {
|
||||||
|
if value, ok := enumProperty(view, Fit, view.Session(), 0); ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImageViewVerticalAlign return the vertical align of an ImageView subview: TopAlign (0), BottomAlign (1), CenterAlign (2)
|
||||||
|
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
|
||||||
|
func GetImageViewVerticalAlign(view View, subviewID string) int {
|
||||||
|
if align, ok := enumProperty(view, ImageVerticalAlign, view.Session(), LeftAlign); ok {
|
||||||
|
return align
|
||||||
|
}
|
||||||
|
return CenterAlign
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImageViewHorizontalAlign return the vertical align of an ImageView subview: LeftAlign (0), RightAlign (1), CenterAlign (2)
|
||||||
|
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
|
||||||
|
func GetImageViewHorizontalAlign(view View, subviewID string) int {
|
||||||
|
if align, ok := enumProperty(view, ImageHorizontalAlign, view.Session(), LeftAlign); ok {
|
||||||
|
return align
|
||||||
|
}
|
||||||
|
return CenterAlign
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
//resources.init()
|
||||||
|
defaultTheme.init()
|
||||||
|
defaultTheme.addText(defaultThemeText)
|
||||||
|
}
|
|
@ -0,0 +1,271 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// KeyDown is the constant for "key-down-event" property tag.
|
||||||
|
// The "key-down-event" event is fired when a key is pressed.
|
||||||
|
// The main listener format: func(View, KeyEvent).
|
||||||
|
// The additional listener formats: func(KeyEvent), func(View), and func().
|
||||||
|
KeyDownEvent = "key-down-event"
|
||||||
|
|
||||||
|
// KeyPp is the constant for "key-up-event" property tag
|
||||||
|
// The "key-up-event" event is fired when a key is released.
|
||||||
|
// The main listener format: func(View, KeyEvent).
|
||||||
|
// The additional listener formats: func(KeyEvent), func(View), and func().
|
||||||
|
KeyUpEvent = "key-up-event"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyEvent struct {
|
||||||
|
// TimeStamp is the time at which the event was created (in milliseconds).
|
||||||
|
// This value is time since epoch—but in reality, browsers' definitions vary.
|
||||||
|
TimeStamp uint64
|
||||||
|
|
||||||
|
// Key is the key value of the key represented by the event. If the value has a printed representation,
|
||||||
|
// this attribute's value is the same as the char property. Otherwise, it's one of the key value strings
|
||||||
|
// specified in Key values. If the key can't be identified, its value is the string "Unidentified".
|
||||||
|
Key string
|
||||||
|
|
||||||
|
// Code holds a string that identifies the physical key being pressed. The value is not affected
|
||||||
|
// by the current keyboard layout or modifier state, so a particular key will always return the same value.
|
||||||
|
Code string
|
||||||
|
|
||||||
|
// Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false.
|
||||||
|
Repeat bool
|
||||||
|
|
||||||
|
// CtrlKey == true if the control key was down when the event was fired. false otherwise.
|
||||||
|
CtrlKey bool
|
||||||
|
|
||||||
|
// ShiftKey == true if the shift key was down when the event was fired. false otherwise.
|
||||||
|
ShiftKey bool
|
||||||
|
|
||||||
|
// AltKey == true if the alt key was down when the event was fired. false otherwise.
|
||||||
|
AltKey bool
|
||||||
|
|
||||||
|
// MetaKey == true if the meta key was down when the event was fired. false otherwise.
|
||||||
|
MetaKey bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToKeyListeners(value interface{}) ([]func(View, KeyEvent), bool) {
|
||||||
|
if value == nil {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(View, KeyEvent):
|
||||||
|
return []func(View, KeyEvent){value}, true
|
||||||
|
|
||||||
|
case func(KeyEvent):
|
||||||
|
fn := func(view View, event KeyEvent) {
|
||||||
|
value(event)
|
||||||
|
}
|
||||||
|
return []func(View, KeyEvent){fn}, true
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
fn := func(view View, event KeyEvent) {
|
||||||
|
value(view)
|
||||||
|
}
|
||||||
|
return []func(View, KeyEvent){fn}, true
|
||||||
|
|
||||||
|
case func():
|
||||||
|
fn := func(view View, event KeyEvent) {
|
||||||
|
value()
|
||||||
|
}
|
||||||
|
return []func(View, KeyEvent){fn}, true
|
||||||
|
|
||||||
|
case []func(View, KeyEvent):
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
for _, fn := range value {
|
||||||
|
if fn == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case []func(KeyEvent):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, KeyEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event KeyEvent) {
|
||||||
|
v(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []func(View):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, KeyEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event KeyEvent) {
|
||||||
|
v(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []func():
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, KeyEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event KeyEvent) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, KeyEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
switch v := v.(type) {
|
||||||
|
case func(View, KeyEvent):
|
||||||
|
listeners[i] = v
|
||||||
|
|
||||||
|
case func(KeyEvent):
|
||||||
|
listeners[i] = func(view View, event KeyEvent) {
|
||||||
|
v(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
listeners[i] = func(view View, event KeyEvent) {
|
||||||
|
v(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func():
|
||||||
|
listeners[i] = func(view View, event KeyEvent) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyEvents = map[string]struct{ jsEvent, jsFunc string }{
|
||||||
|
KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"},
|
||||||
|
KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setKeyListener(tag string, value interface{}) bool {
|
||||||
|
listeners, ok := valueToKeyListeners(value)
|
||||||
|
if !ok {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if listeners == nil {
|
||||||
|
view.removeKeyListener(tag)
|
||||||
|
} else if js, ok := keyEvents[tag]; ok {
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
if view.created {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) removeKeyListener(tag string) {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
if view.created {
|
||||||
|
if js, ok := keyEvents[tag]; ok {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKeyListeners(view View, subviewID string, tag string) []func(View, KeyEvent) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(tag); value != nil {
|
||||||
|
if result, ok := value.([]func(View, KeyEvent)); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(View, KeyEvent){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyEventsHtml(view View, buffer *strings.Builder) {
|
||||||
|
for tag, js := range keyEvents {
|
||||||
|
if listeners := getKeyListeners(view, "", tag); len(listeners) > 0 {
|
||||||
|
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleKeyEvents(view View, tag string, data DataObject) {
|
||||||
|
listeners := getKeyListeners(view, "", tag)
|
||||||
|
if len(listeners) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getBool := func(tag string) bool {
|
||||||
|
if value, ok := data.PropertyValue(tag); ok && value == "1" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
key, _ := data.PropertyValue("key")
|
||||||
|
code, _ := data.PropertyValue("code")
|
||||||
|
event := KeyEvent{
|
||||||
|
TimeStamp: getTimeStamp(data),
|
||||||
|
Key: key,
|
||||||
|
Code: code,
|
||||||
|
Repeat: getBool("repeat"),
|
||||||
|
CtrlKey: getBool("ctrlKey"),
|
||||||
|
ShiftKey: getBool("shiftKey"),
|
||||||
|
AltKey: getBool("altKey"),
|
||||||
|
MetaKey: getBool("metaKey"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, listener := range listeners {
|
||||||
|
listener(view, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyDownListeners returns the "key-down-event" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetKeyDownListeners(view View, subviewID string) []func(View, KeyEvent) {
|
||||||
|
return getKeyListeners(view, subviewID, KeyDownEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeyUpListeners returns the "key-up-event" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetKeyUpListeners(view View, subviewID string) []func(View, KeyEvent) {
|
||||||
|
return getKeyListeners(view, subviewID, KeyUpEvent)
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
// ListAdapter - the list data source
|
||||||
|
type ListAdapter interface {
|
||||||
|
ListSize() int
|
||||||
|
ListItem(index int, session Session) View
|
||||||
|
IsListItemEnabled(index int) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type textListAdapter struct {
|
||||||
|
items []string
|
||||||
|
views []View
|
||||||
|
params Params
|
||||||
|
}
|
||||||
|
|
||||||
|
type viewListAdapter struct {
|
||||||
|
items []View
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextListAdapter create the new ListAdapter for a string list displaying. The second argument is parameters of a TextView item
|
||||||
|
func NewTextListAdapter(items []string, params Params) ListAdapter {
|
||||||
|
if items == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
adapter := new(textListAdapter)
|
||||||
|
adapter.items = items
|
||||||
|
if params != nil {
|
||||||
|
adapter.params = params
|
||||||
|
} else {
|
||||||
|
adapter.params = Params{}
|
||||||
|
}
|
||||||
|
adapter.views = make([]View, len(items))
|
||||||
|
return adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextListAdapter create the new ListAdapter for a view list displaying
|
||||||
|
func NewViewListAdapter(items []View) ListAdapter {
|
||||||
|
if items != nil {
|
||||||
|
adapter := new(viewListAdapter)
|
||||||
|
adapter.items = items
|
||||||
|
return adapter
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *textListAdapter) ListSize() int {
|
||||||
|
return len(adapter.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *textListAdapter) ListItem(index int, session Session) View {
|
||||||
|
if index < 0 || index >= len(adapter.items) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if adapter.views[index] == nil {
|
||||||
|
adapter.params[Text] = adapter.items[index]
|
||||||
|
adapter.views[index] = NewTextView(session, adapter.params)
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapter.views[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *textListAdapter) IsListItemEnabled(index int) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *viewListAdapter) ListSize() int {
|
||||||
|
return len(adapter.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *viewListAdapter) ListItem(index int, session Session) View {
|
||||||
|
if index >= 0 && index < len(adapter.items) {
|
||||||
|
return adapter.items[index]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *viewListAdapter) IsListItemEnabled(index int) bool {
|
||||||
|
if index >= 0 && index < len(adapter.items) {
|
||||||
|
return !IsDisabled(adapter.items[index])
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
|
||||||
|
TopDownOrientation = 0
|
||||||
|
// StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation
|
||||||
|
StartToEndOrientation = 1
|
||||||
|
// BottomUpOrientation - subviews are arranged from bottom to top
|
||||||
|
BottomUpOrientation = 2
|
||||||
|
// EndToStartOrientation - subviews are arranged from right to left
|
||||||
|
EndToStartOrientation = 3
|
||||||
|
// WrapOff - subviews are scrolled and "true" if a new row/column starts
|
||||||
|
WrapOff = 0
|
||||||
|
// WrapOn - the new row/column starts at bottom/right
|
||||||
|
WrapOn = 1
|
||||||
|
// WrapReverse - the new row/column starts at top/left
|
||||||
|
WrapReverse = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListLayout - list-container of View
|
||||||
|
type ListLayout interface {
|
||||||
|
ViewsContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type listLayoutData struct {
|
||||||
|
viewsContainerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewListLayout create new ListLayout object and return it
|
||||||
|
func NewListLayout(session Session, params Params) ListLayout {
|
||||||
|
view := new(listLayoutData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newListLayout(session Session) View {
|
||||||
|
return NewListLayout(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of ViewsAlignContainer by default values
|
||||||
|
func (listLayout *listLayoutData) Init(session Session) {
|
||||||
|
listLayout.viewsContainerData.Init(session)
|
||||||
|
listLayout.tag = "ListLayout"
|
||||||
|
listLayout.systemClass = "ruiListLayout"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listLayout *listLayoutData) Remove(tag string) {
|
||||||
|
listLayout.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listLayout *listLayoutData) remove(tag string) {
|
||||||
|
listLayout.viewsContainerData.remove(tag)
|
||||||
|
switch tag {
|
||||||
|
case Orientation, Wrap, HorizontalAlign, VerticalAlign:
|
||||||
|
updateCSSStyle(listLayout.htmlID(), listLayout.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listLayout *listLayoutData) Set(tag string, value interface{}) bool {
|
||||||
|
return listLayout.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listLayout *listLayoutData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
listLayout.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if listLayout.viewsContainerData.set(tag, value) {
|
||||||
|
switch tag {
|
||||||
|
case Orientation, Wrap, HorizontalAlign, VerticalAlign:
|
||||||
|
updateCSSStyle(listLayout.htmlID(), listLayout.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listLayout *listLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
if listLayout.views != nil {
|
||||||
|
for _, view := range listLayout.views {
|
||||||
|
view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`})
|
||||||
|
viewHTML(view, buffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListVerticalAlign returns the vertical align of a ListLayout or ListView sibview:
|
||||||
|
// TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3)
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetListVerticalAlign(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return LeftAlign
|
||||||
|
}
|
||||||
|
result, _ := enumProperty(view, VerticalAlign, view.Session(), 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview:
|
||||||
|
// LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3)
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetListHorizontalAlign(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return TopAlign
|
||||||
|
}
|
||||||
|
result, _ := enumProperty(view, HorizontalAlign, view.Session(), 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListOrientation returns the orientation of a ListLayout or ListView subview:
|
||||||
|
// TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3)
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetListOrientation(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
orientation, _ := getOrientation(view, view.Session())
|
||||||
|
return orientation
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetListWrap returns the wrap type of a ListLayout or ListView subview:
|
||||||
|
// WrapOff (0), WrapOn (1), or WrapReverse (2)
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetListWrap(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := enumProperty(view, Wrap, view.Session(), 0); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return WrapOff
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,406 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ClickEvent is the constant for "click-event" property tag
|
||||||
|
// The "click-event" event occurs when the user clicks on the View.
|
||||||
|
// The main listener format: func(View, MouseEvent).
|
||||||
|
// The additional listener formats: func(MouseEvent), func(View), and func().
|
||||||
|
ClickEvent = "click-event"
|
||||||
|
|
||||||
|
// DoubleClickEvent is the constant for "double-click-event" property tag
|
||||||
|
// The "double-click-event" event occurs when the user double clicks on the View.
|
||||||
|
// The main listener format: func(View, MouseEvent).
|
||||||
|
// The additional listener formats: func(MouseEvent), func(View), and func().
|
||||||
|
DoubleClickEvent = "double-click-event"
|
||||||
|
|
||||||
|
// MouseDown is the constant for "mouse-down" property tag.
|
||||||
|
// The "mouse-down" event is fired at a View when a pointing device button is pressed
|
||||||
|
// while the pointer is inside the view.
|
||||||
|
// The main listener format: func(View, MouseEvent).
|
||||||
|
// The additional listener formats: func(MouseEvent), func(View), and func().
|
||||||
|
MouseDown = "mouse-down"
|
||||||
|
|
||||||
|
// MouseUp is the constant for "mouse-up" property tag.
|
||||||
|
// The "mouse-up" event is fired at a View when a button on a pointing device (such as a mouse
|
||||||
|
// or trackpad) is released while the pointer is located inside it.
|
||||||
|
// "mouse-up" events are the counterpoint to "mouse-down" events.
|
||||||
|
// The main listener format: func(View, MouseEvent).
|
||||||
|
// The additional listener formats: func(MouseEvent), func(View), and func().
|
||||||
|
MouseUp = "mouse-up"
|
||||||
|
|
||||||
|
// MouseMove is the constant for "mouse-move" property tag.
|
||||||
|
// The "mouse-move" event is fired at a view when a pointing device (usually a mouse) is moved
|
||||||
|
// while the cursor's hotspot is inside it.
|
||||||
|
// The main listener format: func(View, MouseEvent).
|
||||||
|
// The additional listener formats: func(MouseEvent), func(View), and func().
|
||||||
|
MouseMove = "mouse-move"
|
||||||
|
|
||||||
|
// MouseOut is the constant for "mouse-out" property tag.
|
||||||
|
// The "mouse-out" event is fired at a View when a pointing device (usually a mouse) is used to move
|
||||||
|
// the cursor so that it is no longer contained within the view or one of its children.
|
||||||
|
// "mouse-out" is also delivered to a view if the cursor enters a child view,
|
||||||
|
// because the child view obscures the visible area of the view.
|
||||||
|
// The main listener format: func(View, MouseEvent).
|
||||||
|
// The additional listener formats: func(MouseEvent), func(View), and func().
|
||||||
|
MouseOut = "mouse-out"
|
||||||
|
|
||||||
|
// MouseOver is the constant for "mouse-over" property tag.
|
||||||
|
// The "mouse-over" event is fired at a View when a pointing device (such as a mouse or trackpad)
|
||||||
|
// is used to move the cursor onto the view or one of its child views.
|
||||||
|
// The main listener formats: func(View, MouseEvent).
|
||||||
|
MouseOver = "mouse-over"
|
||||||
|
|
||||||
|
// ContextMenuEvent is the constant for "context-menu-event" property tag
|
||||||
|
// The "context-menu-event" event occurs when the user calls the context menu by the right mouse clicking.
|
||||||
|
// The main listener format: func(View, MouseEvent).
|
||||||
|
ContextMenuEvent = "context-menu-event"
|
||||||
|
|
||||||
|
// PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state
|
||||||
|
PrimaryMouseButton = 0
|
||||||
|
// AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button
|
||||||
|
// or the middle button (if present)
|
||||||
|
AuxiliaryMouseButton = 1
|
||||||
|
// SecondaryMouseButton is a number of the secondary pressed button, usually the right button
|
||||||
|
SecondaryMouseButton = 2
|
||||||
|
// MouseButton4 is a number of the fourth button, typically the Browser Back button
|
||||||
|
MouseButton4 = 3
|
||||||
|
// MouseButton5 is a number of the fifth button, typically the Browser Forward button
|
||||||
|
MouseButton5 = 4
|
||||||
|
|
||||||
|
// PrimaryMouseMask is the mask of the primary button (usually the left button)
|
||||||
|
PrimaryMouseMask = 1
|
||||||
|
// SecondaryMouseMask is the mask of the secondary button (usually the right button)
|
||||||
|
SecondaryMouseMask = 2
|
||||||
|
// AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button)
|
||||||
|
AuxiliaryMouseMask = 4
|
||||||
|
// MouseMask4 is the mask of the 4th button (typically the "Browser Back" button)
|
||||||
|
MouseMask4 = 8
|
||||||
|
//MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button)
|
||||||
|
MouseMask5 = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
type MouseEvent struct {
|
||||||
|
// TimeStamp is the time at which the event was created (in milliseconds).
|
||||||
|
// This value is time since epoch—but in reality, browsers' definitions vary.
|
||||||
|
TimeStamp uint64
|
||||||
|
|
||||||
|
// Button indicates which button was pressed on the mouse to trigger the event:
|
||||||
|
// PrimaryMouseButton (0), AuxiliaryMouseButton (1), SecondaryMouseButton (2),
|
||||||
|
// MouseButton4 (3), and MouseButton5 (4)
|
||||||
|
Button int
|
||||||
|
|
||||||
|
// Buttons indicates which buttons are pressed on the mouse (or other input device)
|
||||||
|
// when a mouse event is triggered. Each button that can be pressed is represented by a given mask:
|
||||||
|
// PrimaryMouseMask (1), SecondaryMouseMask (2), AuxiliaryMouseMask (4), MouseMask4 (8), and MouseMask5 (16)
|
||||||
|
Buttons int
|
||||||
|
|
||||||
|
// X provides the horizontal coordinate within the view's viewport.
|
||||||
|
X float64
|
||||||
|
// Y provides the vertical coordinate within the view's viewport.
|
||||||
|
Y float64
|
||||||
|
|
||||||
|
// ClientX provides the horizontal coordinate within the application's viewport at which the event occurred.
|
||||||
|
ClientX float64
|
||||||
|
// ClientY provides the vertical coordinate within the application's viewport at which the event occurred.
|
||||||
|
ClientY float64
|
||||||
|
|
||||||
|
// ScreenX provides the horizontal coordinate (offset) of the mouse pointer in global (screen) coordinates.
|
||||||
|
ScreenX float64
|
||||||
|
// ScreenY provides the vertical coordinate (offset) of the mouse pointer in global (screen) coordinates.
|
||||||
|
ScreenY float64
|
||||||
|
|
||||||
|
// CtrlKey == true if the control key was down when the event was fired. false otherwise.
|
||||||
|
CtrlKey bool
|
||||||
|
// ShiftKey == true if the shift key was down when the event was fired. false otherwise.
|
||||||
|
ShiftKey bool
|
||||||
|
// AltKey == true if the alt key was down when the event was fired. false otherwise.
|
||||||
|
AltKey bool
|
||||||
|
// MetaKey == true if the meta key was down when the event was fired. false otherwise.
|
||||||
|
MetaKey bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToMouseListeners(value interface{}) ([]func(View, MouseEvent), bool) {
|
||||||
|
if value == nil {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(View, MouseEvent):
|
||||||
|
return []func(View, MouseEvent){value}, true
|
||||||
|
|
||||||
|
case func(MouseEvent):
|
||||||
|
fn := func(view View, event MouseEvent) {
|
||||||
|
value(event)
|
||||||
|
}
|
||||||
|
return []func(View, MouseEvent){fn}, true
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
fn := func(view View, event MouseEvent) {
|
||||||
|
value(view)
|
||||||
|
}
|
||||||
|
return []func(View, MouseEvent){fn}, true
|
||||||
|
|
||||||
|
case func():
|
||||||
|
fn := func(view View, event MouseEvent) {
|
||||||
|
value()
|
||||||
|
}
|
||||||
|
return []func(View, MouseEvent){fn}, true
|
||||||
|
|
||||||
|
case []func(View, MouseEvent):
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
for _, fn := range value {
|
||||||
|
if fn == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case []func(MouseEvent):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, MouseEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event MouseEvent) {
|
||||||
|
v(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []func(View):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, MouseEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event MouseEvent) {
|
||||||
|
v(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []func():
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, MouseEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event MouseEvent) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, MouseEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
switch v := v.(type) {
|
||||||
|
case func(View, MouseEvent):
|
||||||
|
listeners[i] = v
|
||||||
|
|
||||||
|
case func(MouseEvent):
|
||||||
|
listeners[i] = func(view View, event MouseEvent) {
|
||||||
|
v(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
listeners[i] = func(view View, event MouseEvent) {
|
||||||
|
v(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func():
|
||||||
|
listeners[i] = func(view View, event MouseEvent) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var mouseEvents = map[string]struct{ jsEvent, jsFunc string }{
|
||||||
|
ClickEvent: {jsEvent: "onclick", jsFunc: "clickEvent"},
|
||||||
|
DoubleClickEvent: {jsEvent: "ondblclick", jsFunc: "doubleClickEvent"},
|
||||||
|
MouseDown: {jsEvent: "onmousedown", jsFunc: "mouseDownEvent"},
|
||||||
|
MouseUp: {jsEvent: "onmouseup", jsFunc: "mouseUpEvent"},
|
||||||
|
MouseMove: {jsEvent: "onmousemove", jsFunc: "mouseMoveEvent"},
|
||||||
|
MouseOut: {jsEvent: "onmouseout", jsFunc: "mouseOutEvent"},
|
||||||
|
MouseOver: {jsEvent: "onmouseover", jsFunc: "mouseOverEvent"},
|
||||||
|
ContextMenuEvent: {jsEvent: "oncontextmenu", jsFunc: "contextMenuEvent"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setMouseListener(tag string, value interface{}) bool {
|
||||||
|
listeners, ok := valueToMouseListeners(value)
|
||||||
|
if !ok {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if listeners == nil {
|
||||||
|
view.removeMouseListener(tag)
|
||||||
|
} else if js, ok := mouseEvents[tag]; ok {
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
if view.created {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) removeMouseListener(tag string) {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
if view.created {
|
||||||
|
if js, ok := mouseEvents[tag]; ok {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMouseListeners(view View, subviewID string, tag string) []func(View, MouseEvent) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(tag); value != nil {
|
||||||
|
if result, ok := value.([]func(View, MouseEvent)); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(View, MouseEvent){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mouseEventsHtml(view View, buffer *strings.Builder) {
|
||||||
|
for tag, js := range mouseEvents {
|
||||||
|
if value := view.getRaw(tag); value != nil {
|
||||||
|
if listeners, ok := value.([]func(View, MouseEvent)); ok && len(listeners) > 0 {
|
||||||
|
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeStamp(data DataObject) uint64 {
|
||||||
|
if value, ok := data.PropertyValue("timeStamp"); ok {
|
||||||
|
if index := strings.Index(value, "."); index > 0 {
|
||||||
|
value = value[:index]
|
||||||
|
}
|
||||||
|
if n, err := strconv.ParseUint(value, 10, 64); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event *MouseEvent) init(data DataObject) {
|
||||||
|
|
||||||
|
event.TimeStamp = getTimeStamp(data)
|
||||||
|
event.Button = dataIntProperty(data, "button")
|
||||||
|
event.Buttons = dataIntProperty(data, "buttons")
|
||||||
|
event.X = dataFloatProperty(data, "x")
|
||||||
|
event.Y = dataFloatProperty(data, "y")
|
||||||
|
event.ClientX = dataFloatProperty(data, "clientX")
|
||||||
|
event.ClientY = dataFloatProperty(data, "clientY")
|
||||||
|
event.ScreenX = dataFloatProperty(data, "screenX")
|
||||||
|
event.ScreenY = dataFloatProperty(data, "screenY")
|
||||||
|
event.CtrlKey = dataBoolProperty(data, "ctrlKey")
|
||||||
|
event.ShiftKey = dataBoolProperty(data, "shiftKey")
|
||||||
|
event.AltKey = dataBoolProperty(data, "altKey")
|
||||||
|
event.MetaKey = dataBoolProperty(data, "metaKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleMouseEvents(view View, tag string, data DataObject) {
|
||||||
|
listeners := getMouseListeners(view, "", tag)
|
||||||
|
if len(listeners) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var event MouseEvent
|
||||||
|
event.init(data)
|
||||||
|
|
||||||
|
for _, listener := range listeners {
|
||||||
|
listener(view, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClickListeners returns the "click-event" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetClickListeners(view View, subviewID string) []func(View, MouseEvent) {
|
||||||
|
return getMouseListeners(view, subviewID, ClickEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDoubleClickListeners returns the "double-click-event" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent) {
|
||||||
|
return getMouseListeners(view, subviewID, DoubleClickEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContextMenuListeners returns the "context-menu" listener list.
|
||||||
|
// If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent) {
|
||||||
|
return getMouseListeners(view, subviewID, ContextMenuEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMouseDownListeners returns the "mouse-down" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetMouseDownListeners(view View, subviewID string) []func(View, MouseEvent) {
|
||||||
|
return getMouseListeners(view, subviewID, MouseDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMouseUpListeners returns the "mouse-up" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetMouseUpListeners(view View, subviewID string) []func(View, MouseEvent) {
|
||||||
|
return getMouseListeners(view, subviewID, MouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMouseMoveListeners returns the "mouse-move" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetMouseMoveListeners(view View, subviewID string) []func(View, MouseEvent) {
|
||||||
|
return getMouseListeners(view, subviewID, MouseMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMouseOverListeners returns the "mouse-over" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetMouseOverListeners(view View, subviewID string) []func(View, MouseEvent) {
|
||||||
|
return getMouseListeners(view, subviewID, MouseOver)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMouseOutListeners returns the "mouse-out" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetMouseOutListeners(view View, subviewID string) []func(View, MouseEvent) {
|
||||||
|
return getMouseListeners(view, subviewID, MouseOut)
|
||||||
|
}
|
|
@ -0,0 +1,371 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NumberChangedEvent = "number-changed"
|
||||||
|
NumberPickerType = "number-picker-type"
|
||||||
|
NumberPickerMin = "number-picker-min"
|
||||||
|
NumberPickerMax = "number-picker-max"
|
||||||
|
NumberPickerStep = "number-picker-step"
|
||||||
|
NumberPickerValue = "number-picker-value"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor
|
||||||
|
NumberEditor = 0
|
||||||
|
// NumberSlider - type of NumberPicker. NumberPicker is presented by slider
|
||||||
|
NumberSlider = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// NumberPicker - NumberPicker view
|
||||||
|
type NumberPicker interface {
|
||||||
|
View
|
||||||
|
}
|
||||||
|
|
||||||
|
type numberPickerData struct {
|
||||||
|
viewData
|
||||||
|
numberChangedListeners []func(NumberPicker, float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNumberPicker create new NumberPicker object and return it
|
||||||
|
func NewNumberPicker(session Session, params Params) NumberPicker {
|
||||||
|
view := new(numberPickerData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNumberPicker(session Session) View {
|
||||||
|
return NewNumberPicker(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) Init(session Session) {
|
||||||
|
picker.viewData.Init(session)
|
||||||
|
picker.tag = "NumberPicker"
|
||||||
|
picker.numberChangedListeners = []func(NumberPicker, float64){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case Type, Min, Max, Step, Value:
|
||||||
|
return "number-picker-" + tag
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) Remove(tag string) {
|
||||||
|
picker.remove(picker.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case NumberChangedEvent:
|
||||||
|
if len(picker.numberChangedListeners) > 0 {
|
||||||
|
picker.numberChangedListeners = []func(NumberPicker, float64){}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
picker.viewData.remove(tag)
|
||||||
|
picker.propertyChanged(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) Set(tag string, value interface{}) bool {
|
||||||
|
return picker.set(picker.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
picker.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case NumberChangedEvent:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(NumberPicker, float64):
|
||||||
|
picker.numberChangedListeners = []func(NumberPicker, float64){value}
|
||||||
|
|
||||||
|
case func(float64):
|
||||||
|
fn := func(view NumberPicker, newValue float64) {
|
||||||
|
value(newValue)
|
||||||
|
}
|
||||||
|
picker.numberChangedListeners = []func(NumberPicker, float64){fn}
|
||||||
|
|
||||||
|
case []func(NumberPicker, float64):
|
||||||
|
picker.numberChangedListeners = value
|
||||||
|
|
||||||
|
case []func(float64):
|
||||||
|
listeners := make([]func(NumberPicker, float64), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[i] = func(view NumberPicker, newValue float64) {
|
||||||
|
val(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picker.numberChangedListeners = listeners
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
listeners := make([]func(NumberPicker, float64), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := val.(type) {
|
||||||
|
case func(NumberPicker, float64):
|
||||||
|
listeners[i] = val
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picker.numberChangedListeners = listeners
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case NumberPickerValue:
|
||||||
|
oldValue := GetNumberPickerValue(picker, "")
|
||||||
|
min, max := GetNumberPickerMinMax(picker, "")
|
||||||
|
if picker.setFloatProperty(NumberPickerValue, value, min, max) {
|
||||||
|
newValue := GetNumberPickerValue(picker, "")
|
||||||
|
if oldValue != newValue {
|
||||||
|
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), newValue))
|
||||||
|
for _, listener := range picker.numberChangedListeners {
|
||||||
|
listener(picker, newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if picker.viewData.set(tag, value) {
|
||||||
|
picker.propertyChanged(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) propertyChanged(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case NumberPickerType:
|
||||||
|
if GetNumberPickerType(picker, "") == NumberSlider {
|
||||||
|
updateProperty(picker.htmlID(), "type", "range", picker.session)
|
||||||
|
} else {
|
||||||
|
updateProperty(picker.htmlID(), "type", "number", picker.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case NumberPickerMin:
|
||||||
|
min, _ := GetNumberPickerMinMax(picker, "")
|
||||||
|
updateProperty(picker.htmlID(), Min, strconv.FormatFloat(min, 'f', -1, 32), picker.session)
|
||||||
|
|
||||||
|
case NumberPickerMax:
|
||||||
|
_, max := GetNumberPickerMinMax(picker, "")
|
||||||
|
updateProperty(picker.htmlID(), Max, strconv.FormatFloat(max, 'f', -1, 32), picker.session)
|
||||||
|
|
||||||
|
case NumberPickerStep:
|
||||||
|
if step := GetNumberPickerStep(picker, ""); step > 0 {
|
||||||
|
updateProperty(picker.htmlID(), Step, strconv.FormatFloat(step, 'f', -1, 32), picker.session)
|
||||||
|
} else {
|
||||||
|
updateProperty(picker.htmlID(), Step, "any", picker.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case NumberPickerValue:
|
||||||
|
value := GetNumberPickerValue(picker, "")
|
||||||
|
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), value))
|
||||||
|
for _, listener := range picker.numberChangedListeners {
|
||||||
|
listener(picker, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) Get(tag string) interface{} {
|
||||||
|
return picker.get(picker.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) get(tag string) interface{} {
|
||||||
|
switch tag {
|
||||||
|
case NumberChangedEvent:
|
||||||
|
return picker.numberChangedListeners
|
||||||
|
|
||||||
|
default:
|
||||||
|
return picker.viewData.get(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) htmlTag() string {
|
||||||
|
return "input"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
picker.viewData.htmlProperties(self, buffer)
|
||||||
|
|
||||||
|
if GetNumberPickerType(picker, "") == NumberSlider {
|
||||||
|
buffer.WriteString(` type="range"`)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` type="number"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
min, max := GetNumberPickerMinMax(picker, "")
|
||||||
|
buffer.WriteString(` min="`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(min, 'f', -1, 64))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
|
||||||
|
buffer.WriteString(` max="`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(max, 'f', -1, 64))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
|
||||||
|
step := GetNumberPickerStep(picker, "")
|
||||||
|
if step != 0 {
|
||||||
|
buffer.WriteString(` step="`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(step, 'f', -1, 64))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` step="any"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(` value="`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(GetNumberPickerValue(picker, ""), 'f', -1, 64))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
|
||||||
|
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||||
|
if IsDisabled(self) {
|
||||||
|
buffer.WriteString(` disabled`)
|
||||||
|
}
|
||||||
|
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
switch command {
|
||||||
|
case "textChanged":
|
||||||
|
if text, ok := data.PropertyValue("text"); ok {
|
||||||
|
if value, err := strconv.ParseFloat(text, 32); err == nil {
|
||||||
|
oldValue := GetNumberPickerValue(picker, "")
|
||||||
|
picker.properties[NumberPickerValue] = value
|
||||||
|
if value != oldValue {
|
||||||
|
for _, listener := range picker.numberChangedListeners {
|
||||||
|
listener(picker, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return picker.viewData.handleCommand(self, command, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNumberPickerType returns the type of NumberPicker subview. Valid values:
|
||||||
|
// NumberEditor (0) - NumberPicker is presented by editor (default type)
|
||||||
|
// NumberSlider (1) - NumberPicker is presented by slider
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetNumberPickerType(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
t, _ := enumStyledProperty(view, NumberPickerType, NumberEditor)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNumberPickerMinMax returns the min and max value of NumberPicker subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetNumberPickerMinMax(view View, subviewID string) (float64, float64) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
min, ok := floatStyledProperty(view, NumberPickerMin, 0)
|
||||||
|
if !ok {
|
||||||
|
min, _ = floatStyledProperty(view, Min, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
max, ok := floatStyledProperty(view, NumberPickerMax, 1)
|
||||||
|
if !ok {
|
||||||
|
min, _ = floatStyledProperty(view, Max, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if min > max {
|
||||||
|
return max, min
|
||||||
|
}
|
||||||
|
return min, max
|
||||||
|
}
|
||||||
|
return 0, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNumberPickerStep returns the value changing step of NumberPicker subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetNumberPickerStep(view View, subviewID string) float64 {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := floatStyledProperty(view, NumberPickerStep, 0)
|
||||||
|
if !ok {
|
||||||
|
result, _ = floatStyledProperty(view, Step, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, max := GetNumberPickerMinMax(view, "")
|
||||||
|
if result > max {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNumberPickerValue returns the value of NumberPicker subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetNumberPickerValue(view View, subviewID string) float64 {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
min, _ := GetNumberPickerMinMax(view, "")
|
||||||
|
result, ok := floatStyledProperty(view, NumberPickerValue, min)
|
||||||
|
if !ok {
|
||||||
|
result, _ = floatStyledProperty(view, Value, min)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview.
|
||||||
|
// If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetNumberChangedListeners(view View, subviewID string) []func(NumberPicker, float64) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(NumberChangedEvent); value != nil {
|
||||||
|
if listeners, ok := value.([]func(NumberPicker, float64)); ok {
|
||||||
|
return listeners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(NumberPicker, float64){}
|
||||||
|
}
|
|
@ -0,0 +1,153 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutlineProperty interface {
|
||||||
|
Properties
|
||||||
|
ruiStringer
|
||||||
|
fmt.Stringer
|
||||||
|
ViewOutline(session Session) ViewOutline
|
||||||
|
}
|
||||||
|
|
||||||
|
type outlinePropertyData struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOutlineProperty(params Params) OutlineProperty {
|
||||||
|
outline := new(outlinePropertyData)
|
||||||
|
outline.properties = map[string]interface{}{}
|
||||||
|
for tag, value := range params {
|
||||||
|
outline.Set(tag, value)
|
||||||
|
}
|
||||||
|
return outline
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline *outlinePropertyData) ruiString(writer ruiWriter) {
|
||||||
|
writer.startObject("_")
|
||||||
|
|
||||||
|
for _, tag := range []string{Style, Width, ColorProperty} {
|
||||||
|
if value, ok := outline.properties[tag]; ok {
|
||||||
|
writer.writeProperty(Style, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline *outlinePropertyData) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
outline.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline *outlinePropertyData) normalizeTag(tag string) string {
|
||||||
|
return strings.TrimPrefix(strings.ToLower(tag), "outline-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline *outlinePropertyData) Remove(tag string) {
|
||||||
|
delete(outline.properties, outline.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline *outlinePropertyData) Set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
outline.Remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = outline.normalizeTag(tag)
|
||||||
|
switch tag {
|
||||||
|
case Style:
|
||||||
|
return outline.setEnumProperty(Style, value, enumProperties[BorderStyle].values)
|
||||||
|
|
||||||
|
case Width:
|
||||||
|
if width, ok := value.(SizeUnit); ok {
|
||||||
|
switch width.Type {
|
||||||
|
case SizeInFraction, SizeInPercent:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outline.setSizeProperty(Width, value)
|
||||||
|
|
||||||
|
case ColorProperty:
|
||||||
|
return outline.setColorProperty(ColorProperty, value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF(`"%s" property is not compatible with the OutlineProperty`, tag)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline *outlinePropertyData) Get(tag string) interface{} {
|
||||||
|
return outline.propertyList.Get(outline.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline {
|
||||||
|
style, _ := valueToEnum(outline.getRaw(Style), BorderStyle, session, NoneLine)
|
||||||
|
width, _ := sizeProperty(outline, Width, session)
|
||||||
|
color, _ := colorProperty(outline, ColorProperty, session)
|
||||||
|
return ViewOutline{Style: style, Width: width, Color: color}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewOutline describes parameters of a view border
|
||||||
|
type ViewOutline struct {
|
||||||
|
Style int
|
||||||
|
Color Color
|
||||||
|
Width SizeUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline ViewOutline) cssValue(builder cssBuilder) {
|
||||||
|
values := enumProperties[BorderStyle].cssValues
|
||||||
|
if outline.Style > 0 && outline.Style < len(values) && outline.Color.Alpha() > 0 &&
|
||||||
|
outline.Width.Type != Auto && outline.Width.Type != SizeInFraction &&
|
||||||
|
outline.Width.Type != SizeInPercent && outline.Width.Value > 0 {
|
||||||
|
builder.addValues("outline", " ", outline.Width.cssString("0"), values[outline.Style], outline.Color.cssString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (outline ViewOutline) cssString() string {
|
||||||
|
var builder cssValueBuilder
|
||||||
|
outline.cssValue(&builder)
|
||||||
|
return builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOutline(properties Properties) OutlineProperty {
|
||||||
|
if value := properties.Get(Outline); value != nil {
|
||||||
|
if outline, ok := value.(OutlineProperty); ok {
|
||||||
|
return outline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) setOutline(value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case OutlineProperty:
|
||||||
|
style.properties[Outline] = value
|
||||||
|
|
||||||
|
case ViewOutline:
|
||||||
|
style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorProperty: value.Color})
|
||||||
|
|
||||||
|
case ViewBorder:
|
||||||
|
style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorProperty: value.Color})
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
outline := NewOutlineProperty(nil)
|
||||||
|
for _, tag := range []string{Style, Width, ColorProperty} {
|
||||||
|
if text, ok := value.PropertyValue(tag); ok && text != "" {
|
||||||
|
outline.Set(tag, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
style.properties[Outline] = outline
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(Outline, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Path is a path interface
|
||||||
|
type Path interface {
|
||||||
|
// Reset erases the Path
|
||||||
|
Reset()
|
||||||
|
|
||||||
|
// MoveTo begins a new sub-path at the point specified by the given (x, y) coordinates
|
||||||
|
MoveTo(x, y float64)
|
||||||
|
|
||||||
|
// LineTo adds a straight line to the current sub-path by connecting
|
||||||
|
// the sub-path's last point to the specified (x, y) coordinates
|
||||||
|
LineTo(x, y float64)
|
||||||
|
|
||||||
|
// ArcTo adds a circular arc to the current sub-path, using the given control points and radius.
|
||||||
|
// The arc is automatically connected to the path's latest point with a straight line, if necessary.
|
||||||
|
// x0, y0 - coordinates of the first control point;
|
||||||
|
// x1, y1 - coordinates of the second control point;
|
||||||
|
// radius - the arc's radius. Must be non-negative.
|
||||||
|
ArcTo(x0, y0, x1, y1, radius float64)
|
||||||
|
|
||||||
|
// Arc adds a circular arc to the current sub-path.
|
||||||
|
// x, y - coordinates of the arc's center;
|
||||||
|
// radius - the arc's radius. Must be non-negative;
|
||||||
|
// startAngle - the angle at which the arc starts, measured clockwise from the positive
|
||||||
|
// x-axis and expressed in radians.
|
||||||
|
// endAngle - the angle at which the arc ends, measured clockwise from the positive
|
||||||
|
// x-axis and expressed in radians.
|
||||||
|
// clockwise - if true, causes the arc to be drawn clockwise between the start and end angles,
|
||||||
|
// otherwise - counter-clockwise
|
||||||
|
Arc(x, y, radius, startAngle, endAngle float64, clockwise bool)
|
||||||
|
|
||||||
|
// BezierCurveTo adds a cubic Bézier curve to the current sub-path. The starting point is
|
||||||
|
// the latest point in the current path.
|
||||||
|
// cp0x, cp0y - coordinates of the first control point;
|
||||||
|
// cp1x, cp1y - coordinates of the second control point;
|
||||||
|
// x, y - coordinates of the end point.
|
||||||
|
BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64)
|
||||||
|
|
||||||
|
// QuadraticCurveTo adds a quadratic Bézier curve to the current sub-path.
|
||||||
|
// cpx, cpy - coordinates of the control point;
|
||||||
|
// x, y - coordinates of the end point.
|
||||||
|
QuadraticCurveTo(cpx, cpy, x, y float64)
|
||||||
|
|
||||||
|
// Ellipse adds an elliptical arc to the current sub-path
|
||||||
|
// x, y - coordinates of the ellipse's center;
|
||||||
|
// radiusX - the ellipse's major-axis radius. Must be non-negative;
|
||||||
|
// radiusY - the ellipse's minor-axis radius. Must be non-negative;
|
||||||
|
// rotation - the rotation of the ellipse, expressed in radians;
|
||||||
|
// startAngle - the angle at which the ellipse starts, measured clockwise
|
||||||
|
// from the positive x-axis and expressed in radians;
|
||||||
|
// endAngle - the angle at which the ellipse ends, measured clockwise
|
||||||
|
// from the positive x-axis and expressed in radians.
|
||||||
|
// clockwise - if true, draws the ellipse clockwise, otherwise draws counter-clockwise
|
||||||
|
Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool)
|
||||||
|
|
||||||
|
// Close adds a straight line from the current point to the start of the current sub-path.
|
||||||
|
// If the shape has already been closed or has only one point, this function does nothing.
|
||||||
|
Close()
|
||||||
|
|
||||||
|
scriptText() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathData struct {
|
||||||
|
script strings.Builder
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPath creates a new empty Path
|
||||||
|
func NewPath() Path {
|
||||||
|
path := new(pathData)
|
||||||
|
path.script.Grow(4096)
|
||||||
|
path.script.WriteString("\nctx.beginPath();")
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) Reset() {
|
||||||
|
path.script.Reset()
|
||||||
|
path.script.WriteString("\nctx.beginPath();")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) MoveTo(x, y float64) {
|
||||||
|
path.script.WriteString("\nctx.moveTo(")
|
||||||
|
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
|
||||||
|
path.script.WriteString(");")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) LineTo(x, y float64) {
|
||||||
|
path.script.WriteString("\nctx.lineTo(")
|
||||||
|
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
|
||||||
|
path.script.WriteString(");")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) ArcTo(x0, y0, x1, y1, radius float64) {
|
||||||
|
if radius > 0 {
|
||||||
|
path.script.WriteString("\nctx.arcTo(")
|
||||||
|
path.script.WriteString(strconv.FormatFloat(x0, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(y0, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(x1, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(y1, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(radius, 'g', -1, 64))
|
||||||
|
path.script.WriteString(");")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) {
|
||||||
|
if radius > 0 {
|
||||||
|
path.script.WriteString("\nctx.arc(")
|
||||||
|
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(radius, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(startAngle, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(endAngle, 'g', -1, 64))
|
||||||
|
if !clockwise {
|
||||||
|
path.script.WriteString(",true);")
|
||||||
|
} else {
|
||||||
|
path.script.WriteString(");")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) {
|
||||||
|
path.script.WriteString("\nctx.bezierCurveTo(")
|
||||||
|
path.script.WriteString(strconv.FormatFloat(cp0x, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(cp0y, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(cp1x, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(cp1y, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
|
||||||
|
path.script.WriteString(");")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) QuadraticCurveTo(cpx, cpy, x, y float64) {
|
||||||
|
path.script.WriteString("\nctx.quadraticCurveTo(")
|
||||||
|
path.script.WriteString(strconv.FormatFloat(cpx, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(cpy, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
|
||||||
|
path.script.WriteString(");")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) {
|
||||||
|
if radiusX > 0 && radiusY > 0 {
|
||||||
|
path.script.WriteString("\nctx.ellipse(")
|
||||||
|
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(radiusX, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(radiusY, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(rotation, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(startAngle, 'g', -1, 64))
|
||||||
|
path.script.WriteRune(',')
|
||||||
|
path.script.WriteString(strconv.FormatFloat(endAngle, 'g', -1, 64))
|
||||||
|
if !clockwise {
|
||||||
|
path.script.WriteString(",true);")
|
||||||
|
} else {
|
||||||
|
path.script.WriteString(");")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) Close() {
|
||||||
|
path.script.WriteString("\nctx.close();")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (path *pathData) scriptText() string {
|
||||||
|
return path.script.String()
|
||||||
|
}
|
|
@ -0,0 +1,341 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PointerDown is the constant for "pointer-down" property tag.
|
||||||
|
// The "pointer-down" event is fired when a pointer becomes active. For mouse, it is fired when
|
||||||
|
// the device transitions from no buttons depressed to at least one button depressed.
|
||||||
|
// For touch, it is fired when physical contact is made with the digitizer.
|
||||||
|
// For pen, it is fired when the stylus makes physical contact with the digitizer.
|
||||||
|
// The main listener format: func(View, PointerEvent).
|
||||||
|
// The additional listener formats: func(PointerEvent), func(View), and func().
|
||||||
|
PointerDown = "pointer-down"
|
||||||
|
|
||||||
|
// PointerUp is the constant for "pointer-up" property tag.
|
||||||
|
// The "pointer-up" event is fired when a pointer is no longer active.
|
||||||
|
// The main listener format: func(View, PointerEvent).
|
||||||
|
// The additional listener formats: func(PointerEvent), func(View), and func().
|
||||||
|
PointerUp = "pointer-up"
|
||||||
|
|
||||||
|
// PointerMove is the constant for "pointer-move" property tag.
|
||||||
|
// The "pointer-move" event is fired when a pointer changes coordinates.
|
||||||
|
// The main listener format: func(View, PointerEvent).
|
||||||
|
// The additional listener formats: func(PointerEvent), func(View), and func().
|
||||||
|
PointerMove = "pointer-move"
|
||||||
|
|
||||||
|
// PointerCancel is the constant for "pointer-cancel" property tag.
|
||||||
|
// The "pointer-cancel" event is fired if the pointer will no longer be able to generate events
|
||||||
|
// (for example the related device is deactivated).
|
||||||
|
// The main listener format: func(View, PointerEvent).
|
||||||
|
// The additional listener formats: func(PointerEvent), func(View), and func().
|
||||||
|
PointerCancel = "pointer-cancel"
|
||||||
|
|
||||||
|
// PointerOut is the constant for "pointer-out" property tag.
|
||||||
|
// The "pointer-out" event is fired for several reasons including: pointing device is moved out
|
||||||
|
// of the hit test boundaries of an element; firing the pointerup event for a device
|
||||||
|
// that does not support hover (see "pointer-up"); after firing the pointercancel event (see "pointer-cancel");
|
||||||
|
// when a pen stylus leaves the hover range detectable by the digitizer.
|
||||||
|
// The main listener format: func(View, PointerEvent).
|
||||||
|
// The additional listener formats: func(PointerEvent), func(View), and func().
|
||||||
|
PointerOut = "pointer-out"
|
||||||
|
|
||||||
|
// PointerOver is the constant for "pointer-over" property tag.
|
||||||
|
// The "pointer-over" event is fired when a pointing device is moved into an view's hit test boundaries.
|
||||||
|
// The main listener format: func(View, PointerEvent).
|
||||||
|
// The additional listener formats: func(PointerEvent), func(View), and func().
|
||||||
|
PointerOver = "pointer-over"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PointerEvent struct {
|
||||||
|
MouseEvent
|
||||||
|
|
||||||
|
// PointerID is a unique identifier for the pointer causing the event.
|
||||||
|
PointerID int
|
||||||
|
|
||||||
|
// Width is the width (magnitude on the X axis), in pixels, of the contact geometry of the pointer.
|
||||||
|
Width float64
|
||||||
|
// Height is the height (magnitude on the Y axis), in pixels, of the contact geometry of the pointer.
|
||||||
|
Height float64
|
||||||
|
|
||||||
|
// Pressure is the normalized pressure of the pointer input in the range 0 to 1, where 0 and 1 represent
|
||||||
|
// the minimum and maximum pressure the hardware is capable of detecting, respectively.
|
||||||
|
Pressure float64
|
||||||
|
|
||||||
|
// TangentialPressure is the normalized tangential pressure of the pointer input (also known
|
||||||
|
// as barrel pressure or cylinder stress) in the range -1 to 1, where 0 is the neutral position of the control.
|
||||||
|
TangentialPressure float64
|
||||||
|
|
||||||
|
// TiltX is the plane angle (in degrees, in the range of -90 to 90) between the Y–Z plane
|
||||||
|
// and the plane containing both the pointer (e.g. pen stylus) axis and the Y axis.
|
||||||
|
TiltX float64
|
||||||
|
|
||||||
|
// TiltY is the plane angle (in degrees, in the range of -90 to 90) between the X–Z plane
|
||||||
|
// and the plane containing both the pointer (e.g. pen stylus) axis and the X axis.
|
||||||
|
TiltY float64
|
||||||
|
|
||||||
|
// Twist is the clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees,
|
||||||
|
// with a value in the range 0 to 359.
|
||||||
|
Twist float64
|
||||||
|
|
||||||
|
// PointerType indicates the device type that caused the event ("mouse", "pen", "touch", etc.)
|
||||||
|
PointerType string
|
||||||
|
|
||||||
|
// IsPrimary indicates if the pointer represents the primary pointer of this pointer type.
|
||||||
|
IsPrimary bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToPointerListeners(value interface{}) ([]func(View, PointerEvent), bool) {
|
||||||
|
if value == nil {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(View, PointerEvent):
|
||||||
|
return []func(View, PointerEvent){value}, true
|
||||||
|
|
||||||
|
case func(PointerEvent):
|
||||||
|
fn := func(view View, event PointerEvent) {
|
||||||
|
value(event)
|
||||||
|
}
|
||||||
|
return []func(View, PointerEvent){fn}, true
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
fn := func(view View, event PointerEvent) {
|
||||||
|
value(view)
|
||||||
|
}
|
||||||
|
return []func(View, PointerEvent){fn}, true
|
||||||
|
|
||||||
|
case func():
|
||||||
|
fn := func(view View, event PointerEvent) {
|
||||||
|
value()
|
||||||
|
}
|
||||||
|
return []func(View, PointerEvent){fn}, true
|
||||||
|
|
||||||
|
case []func(View, PointerEvent):
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
for _, fn := range value {
|
||||||
|
if fn == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case []func(PointerEvent):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, PointerEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event PointerEvent) {
|
||||||
|
v(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []func(View):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, PointerEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event PointerEvent) {
|
||||||
|
v(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []func():
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, PointerEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event PointerEvent) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, PointerEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
switch v := v.(type) {
|
||||||
|
case func(View, PointerEvent):
|
||||||
|
listeners[i] = v
|
||||||
|
|
||||||
|
case func(PointerEvent):
|
||||||
|
listeners[i] = func(view View, event PointerEvent) {
|
||||||
|
v(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
listeners[i] = func(view View, event PointerEvent) {
|
||||||
|
v(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func():
|
||||||
|
listeners[i] = func(view View, event PointerEvent) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var pointerEvents = map[string]struct{ jsEvent, jsFunc string }{
|
||||||
|
PointerDown: {jsEvent: "onpointerdown", jsFunc: "pointerDownEvent"},
|
||||||
|
PointerUp: {jsEvent: "onpointerup", jsFunc: "pointerUpEvent"},
|
||||||
|
PointerMove: {jsEvent: "onpointermove", jsFunc: "pointerMoveEvent"},
|
||||||
|
PointerCancel: {jsEvent: "onpointercancel", jsFunc: "pointerCancelEvent"},
|
||||||
|
PointerOut: {jsEvent: "onpointerout", jsFunc: "pointerOutEvent"},
|
||||||
|
PointerOver: {jsEvent: "onpointerover", jsFunc: "pointerOverEvent"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setPointerListener(tag string, value interface{}) bool {
|
||||||
|
listeners, ok := valueToPointerListeners(value)
|
||||||
|
if !ok {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if listeners == nil {
|
||||||
|
view.removePointerListener(tag)
|
||||||
|
} else if js, ok := pointerEvents[tag]; ok {
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
if view.created {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) removePointerListener(tag string) {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
if view.created {
|
||||||
|
if js, ok := pointerEvents[tag]; ok {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPointerListeners(view View, subviewID string, tag string) []func(View, PointerEvent) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(tag); value != nil {
|
||||||
|
if result, ok := value.([]func(View, PointerEvent)); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(View, PointerEvent){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pointerEventsHtml(view View, buffer *strings.Builder) {
|
||||||
|
for tag, js := range pointerEvents {
|
||||||
|
if value := view.getRaw(tag); value != nil {
|
||||||
|
if listeners, ok := value.([]func(View, PointerEvent)); ok && len(listeners) > 0 {
|
||||||
|
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event *PointerEvent) init(data DataObject) {
|
||||||
|
event.MouseEvent.init(data)
|
||||||
|
|
||||||
|
event.PointerID = dataIntProperty(data, "pointerId")
|
||||||
|
event.Width = dataFloatProperty(data, "width")
|
||||||
|
event.Height = dataFloatProperty(data, "height")
|
||||||
|
event.Pressure = dataFloatProperty(data, "pressure")
|
||||||
|
event.TangentialPressure = dataFloatProperty(data, "tangentialPressure")
|
||||||
|
event.TiltX = dataFloatProperty(data, "tiltX")
|
||||||
|
event.TiltY = dataFloatProperty(data, "tiltY")
|
||||||
|
event.Twist = dataFloatProperty(data, "twist")
|
||||||
|
value, _ := data.PropertyValue("pointerType")
|
||||||
|
event.PointerType = value
|
||||||
|
event.IsPrimary = dataBoolProperty(data, "isPrimary")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePointerEvents(view View, tag string, data DataObject) {
|
||||||
|
listeners := getPointerListeners(view, "", tag)
|
||||||
|
if len(listeners) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var event PointerEvent
|
||||||
|
event.init(data)
|
||||||
|
|
||||||
|
for _, listener := range listeners {
|
||||||
|
listener(view, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointerDownListeners returns the "pointer-down" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetPointerDownListeners(view View, subviewID string) []func(View, PointerEvent) {
|
||||||
|
return getPointerListeners(view, subviewID, PointerDown)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointerUpListeners returns the "pointer-up" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetPointerUpListeners(view View, subviewID string) []func(View, PointerEvent) {
|
||||||
|
return getPointerListeners(view, subviewID, PointerUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointerMoveListeners returns the "pointer-move" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetPointerMoveListeners(view View, subviewID string) []func(View, PointerEvent) {
|
||||||
|
return getPointerListeners(view, subviewID, PointerMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointerCancelListeners returns the "pointer-cancel" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetPointerCancelListeners(view View, subviewID string) []func(View, PointerEvent) {
|
||||||
|
return getPointerListeners(view, subviewID, PointerCancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointerOverListeners returns the "pointer-over" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetPointerOverListeners(view View, subviewID string) []func(View, PointerEvent) {
|
||||||
|
return getPointerListeners(view, subviewID, PointerOver)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPointerOutListeners returns the "pointer-out" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetPointerOutListeners(view View, subviewID string) []func(View, PointerEvent) {
|
||||||
|
return getPointerListeners(view, subviewID, PointerOut)
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Title is the Popup string property
|
||||||
|
Title = "title"
|
||||||
|
// TitleStyle is the Popup string property
|
||||||
|
TitleStyle = "title-style"
|
||||||
|
// CloseButton is the Popup bool property
|
||||||
|
CloseButton = "close-button"
|
||||||
|
// OutsideClose is the Popup bool property
|
||||||
|
OutsideClose = "outside-close"
|
||||||
|
Buttons = "buttons"
|
||||||
|
ButtonsAlign = "buttons-align"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PopupButton struct {
|
||||||
|
Title string
|
||||||
|
OnClick func(Popup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Popup interface
|
||||||
|
type Popup interface {
|
||||||
|
//Properties
|
||||||
|
View() View
|
||||||
|
Session() Session
|
||||||
|
Show()
|
||||||
|
Dismiss()
|
||||||
|
html(buffer *strings.Builder)
|
||||||
|
viewByHTMLID(id string) View
|
||||||
|
}
|
||||||
|
|
||||||
|
type popupData struct {
|
||||||
|
//propertyList
|
||||||
|
layerView View
|
||||||
|
view View
|
||||||
|
}
|
||||||
|
|
||||||
|
type popupManager struct {
|
||||||
|
popups []Popup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupData) init(view View, params Params) {
|
||||||
|
popup.view = view
|
||||||
|
|
||||||
|
props := propertyList{properties: params}
|
||||||
|
session := view.Session()
|
||||||
|
|
||||||
|
var title View = nil
|
||||||
|
titleStyle := "ruiPopupTitle"
|
||||||
|
closeButton, _ := boolProperty(&props, CloseButton, session)
|
||||||
|
outsideClose, _ := boolProperty(&props, OutsideClose, session)
|
||||||
|
vAlign, _ := enumProperty(&props, VerticalAlign, session, CenterAlign)
|
||||||
|
hAlign, _ := enumProperty(&props, HorizontalAlign, session, CenterAlign)
|
||||||
|
buttonsAlign, _ := enumProperty(&props, ButtonsAlign, session, RightAlign)
|
||||||
|
|
||||||
|
buttons := []PopupButton{}
|
||||||
|
if value, ok := params[Buttons]; ok && value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case PopupButton:
|
||||||
|
buttons = []PopupButton{value}
|
||||||
|
|
||||||
|
case []PopupButton:
|
||||||
|
buttons = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popupView := NewGridLayout(view.Session(), Params{
|
||||||
|
Style: "ruiPopup",
|
||||||
|
MaxWidth: Percent(100),
|
||||||
|
MaxHeight: Percent(100),
|
||||||
|
CellVerticalAlign: StretchAlign,
|
||||||
|
CellHorizontalAlign: StretchAlign,
|
||||||
|
ClickEvent: func(View) {},
|
||||||
|
})
|
||||||
|
|
||||||
|
for tag, value := range params {
|
||||||
|
switch tag {
|
||||||
|
case Title:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
title = NewTextView(view.Session(), Params{Text: value})
|
||||||
|
|
||||||
|
case View:
|
||||||
|
title = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(Title, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case TitleStyle:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
titleStyle = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(TitleStyle, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case CloseButton, OutsideClose, VerticalAlign, HorizontalAlign:
|
||||||
|
// do nothing
|
||||||
|
|
||||||
|
default:
|
||||||
|
popupView.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cellHeight []SizeUnit
|
||||||
|
viewRow := 0
|
||||||
|
if title != nil || closeButton {
|
||||||
|
viewRow = 1
|
||||||
|
titleHeight, _ := sizeConstant(popup.Session(), "popupTitleHeight")
|
||||||
|
titleView := NewGridLayout(session, Params{
|
||||||
|
Row: 0,
|
||||||
|
Style: titleStyle,
|
||||||
|
CellWidth: []SizeUnit{Fr(1), titleHeight},
|
||||||
|
CellVerticalAlign: CenterAlign,
|
||||||
|
PaddingLeft: Px(12),
|
||||||
|
})
|
||||||
|
if title != nil {
|
||||||
|
titleView.Append(title)
|
||||||
|
}
|
||||||
|
if closeButton {
|
||||||
|
titleView.Append(NewGridLayout(session, Params{
|
||||||
|
Column: 1,
|
||||||
|
Height: titleHeight,
|
||||||
|
Width: titleHeight,
|
||||||
|
CellHorizontalAlign: CenterAlign,
|
||||||
|
CellVerticalAlign: CenterAlign,
|
||||||
|
TextSize: Px(20),
|
||||||
|
Content: "✕",
|
||||||
|
ClickEvent: func(View) {
|
||||||
|
popup.Dismiss()
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
popupView.Append(titleView)
|
||||||
|
cellHeight = []SizeUnit{AutoSize(), Fr(1)}
|
||||||
|
} else {
|
||||||
|
cellHeight = []SizeUnit{Fr(1)}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Set(Row, viewRow)
|
||||||
|
popupView.Append(view)
|
||||||
|
|
||||||
|
if buttonCount := len(buttons); buttonCount > 0 {
|
||||||
|
cellHeight = append(cellHeight, AutoSize())
|
||||||
|
gap, _ := sizeConstant(session, "popupButtonGap")
|
||||||
|
cellWidth := []SizeUnit{}
|
||||||
|
for i := 0; i < buttonCount; i++ {
|
||||||
|
cellWidth = append(cellWidth, Fr(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonsPanel := NewGridLayout(session, Params{
|
||||||
|
CellWidth: cellWidth,
|
||||||
|
})
|
||||||
|
if gap.Type != Auto && gap.Value > 0 {
|
||||||
|
buttonsPanel.Set(Gap, gap)
|
||||||
|
buttonsPanel.Set(Margin, gap)
|
||||||
|
}
|
||||||
|
|
||||||
|
createButton := func(n int, button PopupButton) Button {
|
||||||
|
return NewButton(session, Params{
|
||||||
|
Column: n,
|
||||||
|
Content: button.Title,
|
||||||
|
ClickEvent: func() {
|
||||||
|
if button.OnClick != nil {
|
||||||
|
button.OnClick(popup)
|
||||||
|
} else {
|
||||||
|
popup.Dismiss()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for i, button := range buttons {
|
||||||
|
buttonsPanel.Append(createButton(i, button))
|
||||||
|
}
|
||||||
|
|
||||||
|
popupView.Append(NewGridLayout(session, Params{
|
||||||
|
Row: viewRow + 1,
|
||||||
|
CellHorizontalAlign: buttonsAlign,
|
||||||
|
Content: buttonsPanel,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
popupView.Set(CellHeight, cellHeight)
|
||||||
|
|
||||||
|
popup.layerView = NewGridLayout(session, Params{
|
||||||
|
Style: "ruiPopupLayer",
|
||||||
|
CellVerticalAlign: vAlign,
|
||||||
|
CellHorizontalAlign: hAlign,
|
||||||
|
Content: popupView,
|
||||||
|
MaxWidth: Percent(100),
|
||||||
|
MaxHeight: Percent(100),
|
||||||
|
})
|
||||||
|
|
||||||
|
if outsideClose {
|
||||||
|
popup.layerView.Set(ClickEvent, func(View) {
|
||||||
|
popup.Dismiss()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup popupData) View() View {
|
||||||
|
return popup.view
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupData) Session() Session {
|
||||||
|
return popup.view.Session()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupData) Dismiss() {
|
||||||
|
popup.Session().popupManager().dismissPopup(popup)
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupData) Show() {
|
||||||
|
popup.Session().popupManager().showPopup(popup)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupData) html(buffer *strings.Builder) {
|
||||||
|
|
||||||
|
viewHTML(popup.layerView, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupData) viewByHTMLID(id string) View {
|
||||||
|
return viewByHTMLID(id, popup.layerView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPopup creates a new Popup
|
||||||
|
func NewPopup(view View, param Params) Popup {
|
||||||
|
if view == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
popup := new(popupData)
|
||||||
|
popup.init(view, param)
|
||||||
|
return popup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *popupManager) updatePopupLayerInnerHTML(session Session) {
|
||||||
|
if manager.popups == nil {
|
||||||
|
manager.popups = []Popup{}
|
||||||
|
session.runScript(`updateInnerHTML('ruiPopupLayer', '');`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(`updateInnerHTML('ruiPopupLayer', '`)
|
||||||
|
for _, p := range manager.popups {
|
||||||
|
p.html(buffer)
|
||||||
|
}
|
||||||
|
buffer.WriteString(`');`)
|
||||||
|
session.runScript(buffer.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *popupManager) showPopup(popup Popup) {
|
||||||
|
if popup == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session := popup.Session()
|
||||||
|
if manager.popups == nil || len(manager.popups) == 0 {
|
||||||
|
manager.popups = []Popup{popup}
|
||||||
|
} else {
|
||||||
|
manager.popups = append(manager.popups, popup)
|
||||||
|
}
|
||||||
|
manager.updatePopupLayerInnerHTML(session)
|
||||||
|
updateCSSProperty("ruiPopupLayer", "visibility", "visible", session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *popupManager) dismissPopup(popup Popup) {
|
||||||
|
if manager.popups == nil {
|
||||||
|
manager.popups = []Popup{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(manager.popups)
|
||||||
|
if count <= 0 || popup == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session := popup.Session()
|
||||||
|
if manager.popups[count-1] == popup {
|
||||||
|
if count == 1 {
|
||||||
|
manager.popups = []Popup{}
|
||||||
|
updateCSSProperty("ruiPopupLayer", "visibility", "hidden", session)
|
||||||
|
session.runScript(`updateInnerHTML('ruiPopupLayer', '');`)
|
||||||
|
} else {
|
||||||
|
manager.popups = manager.popups[:count-1]
|
||||||
|
manager.updatePopupLayerInnerHTML(session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, p := range manager.popups {
|
||||||
|
if p == popup {
|
||||||
|
if n == 0 {
|
||||||
|
manager.popups = manager.popups[1:]
|
||||||
|
} else {
|
||||||
|
manager.popups = append(manager.popups[:n], manager.popups[n+1:]...)
|
||||||
|
}
|
||||||
|
manager.updatePopupLayerInnerHTML(session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
// ShowMessage displays the popup with text message
|
||||||
|
func ShowMessage(title, text string, session Session) {
|
||||||
|
textView := NewTextView(session, Params{
|
||||||
|
Text: text,
|
||||||
|
Style: "ruiMessageText",
|
||||||
|
})
|
||||||
|
params := Params{
|
||||||
|
CloseButton: true,
|
||||||
|
OutsideClose: true,
|
||||||
|
}
|
||||||
|
if title != "" {
|
||||||
|
params[Title] = title
|
||||||
|
}
|
||||||
|
NewPopup(textView, params).Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) {
|
||||||
|
textView := NewTextView(session, Params{
|
||||||
|
Text: text,
|
||||||
|
Style: "ruiMessageText",
|
||||||
|
})
|
||||||
|
params := Params{
|
||||||
|
CloseButton: false,
|
||||||
|
OutsideClose: false,
|
||||||
|
Buttons: []PopupButton{
|
||||||
|
{
|
||||||
|
Title: "No",
|
||||||
|
OnClick: func(popup Popup) {
|
||||||
|
popup.Dismiss()
|
||||||
|
if onNo != nil {
|
||||||
|
onNo()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Yes",
|
||||||
|
OnClick: func(popup Popup) {
|
||||||
|
popup.Dismiss()
|
||||||
|
if onYes != nil {
|
||||||
|
onYes()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if title != "" {
|
||||||
|
params[Title] = title
|
||||||
|
}
|
||||||
|
NewPopup(textView, params).Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) {
|
||||||
|
textView := NewTextView(session, Params{
|
||||||
|
Text: text,
|
||||||
|
Style: "ruiMessageText",
|
||||||
|
})
|
||||||
|
|
||||||
|
params := Params{
|
||||||
|
CloseButton: false,
|
||||||
|
OutsideClose: false,
|
||||||
|
Buttons: []PopupButton{
|
||||||
|
{
|
||||||
|
Title: "Cancel",
|
||||||
|
OnClick: func(popup Popup) {
|
||||||
|
popup.Dismiss()
|
||||||
|
if onCancel != nil {
|
||||||
|
onCancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "No",
|
||||||
|
OnClick: func(popup Popup) {
|
||||||
|
popup.Dismiss()
|
||||||
|
if onNo != nil {
|
||||||
|
onNo()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Yes",
|
||||||
|
OnClick: func(popup Popup) {
|
||||||
|
popup.Dismiss()
|
||||||
|
if onYes != nil {
|
||||||
|
onYes()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if title != "" {
|
||||||
|
params[Title] = title
|
||||||
|
}
|
||||||
|
NewPopup(textView, params).Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
type popupMenuData struct {
|
||||||
|
items []string
|
||||||
|
session Session
|
||||||
|
popup Popup
|
||||||
|
result func(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupMenuData) itemClick(list ListView, n int) {
|
||||||
|
popup.popup.Dismiss()
|
||||||
|
if popup.result != nil {
|
||||||
|
popup.result(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupMenuData) ListSize() int {
|
||||||
|
return len(popup.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupMenuData) ListItem(index int, session Session) View {
|
||||||
|
return NewTextView(popup.session, Params{
|
||||||
|
Text: popup.items[index],
|
||||||
|
Style: "ruiPopupMenuItem",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (popup *popupMenuData) IsListItemEnabled(index int) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupMenuResult = "popup-menu-result"
|
||||||
|
|
||||||
|
// ShowMenu displays the popup with text message
|
||||||
|
func ShowMenu(session Session, params Params) bool {
|
||||||
|
value, ok := params[Items]
|
||||||
|
if !ok || value == nil {
|
||||||
|
ErrorLog("Unable to show empty menu")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var adapter ListAdapter
|
||||||
|
data := new(popupMenuData)
|
||||||
|
data.session = session
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case []string:
|
||||||
|
data.items = value
|
||||||
|
adapter = data
|
||||||
|
|
||||||
|
case ListAdapter:
|
||||||
|
adapter = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(Items, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok = params[PopupMenuResult]
|
||||||
|
if ok && value != nil {
|
||||||
|
if result, ok := value.(func(int)); ok {
|
||||||
|
data.result = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listView := NewListView(session, Params{
|
||||||
|
Items: adapter,
|
||||||
|
Orientation: TopDownOrientation,
|
||||||
|
ListItemClickedEvent: data.itemClick,
|
||||||
|
})
|
||||||
|
data.popup = NewPopup(listView, params)
|
||||||
|
data.popup.Show()
|
||||||
|
FocusView(listView)
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProgressBarMax = "progress-max"
|
||||||
|
ProgressBarValue = "progress-value"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProgressBar - ProgressBar view
|
||||||
|
type ProgressBar interface {
|
||||||
|
View
|
||||||
|
}
|
||||||
|
|
||||||
|
type progressBarData struct {
|
||||||
|
viewData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProgressBar create new ProgressBar object and return it
|
||||||
|
func NewProgressBar(session Session, params Params) ProgressBar {
|
||||||
|
view := new(progressBarData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProgressBar(session Session) View {
|
||||||
|
return NewProgressBar(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) Init(session Session) {
|
||||||
|
progress.viewData.Init(session)
|
||||||
|
progress.tag = "ProgressBar"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case Max, "progress-bar-max", "progressbar-max":
|
||||||
|
return ProgressBarMax
|
||||||
|
|
||||||
|
case Value, "progress-bar-value", "progressbar-value":
|
||||||
|
return ProgressBarValue
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) Remove(tag string) {
|
||||||
|
progress.remove(progress.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) remove(tag string) {
|
||||||
|
progress.viewData.remove(tag)
|
||||||
|
progress.propertyChanged(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) propertyChanged(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case ProgressBarMax:
|
||||||
|
updateProperty(progress.htmlID(), Max, strconv.FormatFloat(GetProgressBarMax(progress, ""), 'f', -1, 32), progress.session)
|
||||||
|
|
||||||
|
case ProgressBarValue:
|
||||||
|
updateProperty(progress.htmlID(), Value, strconv.FormatFloat(GetProgressBarValue(progress, ""), 'f', -1, 32), progress.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) Set(tag string, value interface{}) bool {
|
||||||
|
return progress.set(progress.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) set(tag string, value interface{}) bool {
|
||||||
|
if progress.viewData.set(tag, value) {
|
||||||
|
progress.propertyChanged(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) Get(tag string) interface{} {
|
||||||
|
return progress.get(progress.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) htmlTag() string {
|
||||||
|
return "progress"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (progress *progressBarData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
progress.viewData.htmlProperties(self, buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(` max="`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(GetProgressBarMax(progress, ""), 'f', -1, 64))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
|
||||||
|
buffer.WriteString(` value="`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(GetProgressBarValue(progress, ""), 'f', -1, 64))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProgressBarMax returns the max value of ProgressBar subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetProgressBarMax(view View, subviewID string) float64 {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := floatStyledProperty(view, ProgressBarMax, 1)
|
||||||
|
if !ok {
|
||||||
|
result, _ = floatStyledProperty(view, Max, 1)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetProgressBarValue returns the value of ProgressBar subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetProgressBarValue(view View, subviewID string) float64 {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := floatStyledProperty(view, ProgressBarValue, 0)
|
||||||
|
if !ok {
|
||||||
|
result, _ = floatStyledProperty(view, Value, 0)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Properties interface of properties map
|
||||||
|
type Properties interface {
|
||||||
|
// Get returns a value of the property with name defined by the argument.
|
||||||
|
// The type of return value depends on the property. If the property is not set then nil is returned.
|
||||||
|
Get(tag string) interface{}
|
||||||
|
getRaw(tag string) interface{}
|
||||||
|
// Set sets the value (second argument) of the property with name defined by the first argument.
|
||||||
|
// Return "true" if the value has been set, in the opposite case "false" are returned and
|
||||||
|
// a description of the error is written to the log
|
||||||
|
Set(tag string, value interface{}) bool
|
||||||
|
setRaw(tag string, value interface{})
|
||||||
|
// Remove removes the property with name defined by the argument
|
||||||
|
Remove(tag string)
|
||||||
|
// Clear removes all properties
|
||||||
|
Clear()
|
||||||
|
// AllTags returns an array of the set properties
|
||||||
|
AllTags() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type propertyList struct {
|
||||||
|
properties map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) init() {
|
||||||
|
properties.properties = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) Get(tag string) interface{} {
|
||||||
|
return properties.getRaw(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) getRaw(tag string) interface{} {
|
||||||
|
if value, ok := properties.properties[tag]; ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setRaw(tag string, value interface{}) {
|
||||||
|
properties.properties[tag] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) Remove(tag string) {
|
||||||
|
delete(properties.properties, strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) remove(tag string) {
|
||||||
|
delete(properties.properties, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) Clear() {
|
||||||
|
properties.properties = map[string]interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) AllTags() []string {
|
||||||
|
tags := make([]string, 0, len(properties.properties))
|
||||||
|
for t := range properties.properties {
|
||||||
|
tags = append(tags, t)
|
||||||
|
}
|
||||||
|
sort.Strings(tags)
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProperties(properties Properties, object DataObject) {
|
||||||
|
count := object.PropertyCount()
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
if node := object.Property(i); node != nil {
|
||||||
|
switch node.Type() {
|
||||||
|
case TextNode:
|
||||||
|
properties.Set(node.Tag(), node.Text())
|
||||||
|
|
||||||
|
case ObjectNode:
|
||||||
|
properties.Set(node.Tag(), node.Object())
|
||||||
|
|
||||||
|
case ArrayNode:
|
||||||
|
properties.Set(node.Tag(), node.ArrayElements())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
/*
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProperties(t *testing.T) {
|
||||||
|
|
||||||
|
createTestLog(t, true)
|
||||||
|
|
||||||
|
list := new(propertyList)
|
||||||
|
list.init()
|
||||||
|
|
||||||
|
if !list.Set("name", "abc") {
|
||||||
|
t.Error(`list.Set("name", "abc") fail`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !list.Has("name") {
|
||||||
|
t.Error(`list.Has("name") fail`)
|
||||||
|
}
|
||||||
|
|
||||||
|
v := list.Get("name")
|
||||||
|
if v == nil {
|
||||||
|
t.Error(`list.Get("name") fail`)
|
||||||
|
}
|
||||||
|
if text, ok := v.(string); ok {
|
||||||
|
if text != "abc" {
|
||||||
|
t.Error(`list.Get("name") != "abc"`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Error(`list.Get("name") is not string`)
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeValues := []interface{}{"@small", "auto", "10px", Pt(20), AutoSize()}
|
||||||
|
for _, value := range sizeValues {
|
||||||
|
if !list.setSizeProperty("size", value) {
|
||||||
|
t.Errorf(`setSizeProperty("size", %v) fail`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failSizeValues := []interface{}{"@small,big", "abc", "10", Color(20), 100}
|
||||||
|
for _, value := range failSizeValues {
|
||||||
|
if list.setSizeProperty("size", value) {
|
||||||
|
t.Errorf(`setSizeProperty("size", %v) success`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
angleValues := []interface{}{"@angle", "2pi", "π", "3deg", "60°", Rad(1.5), Deg(45), 1, 1.5}
|
||||||
|
for _, value := range angleValues {
|
||||||
|
if !list.setAngleProperty("angle", value) {
|
||||||
|
t.Errorf(`setAngleProperty("angle", %v) fail`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failAngleValues := []interface{}{"@angle,2", "pi32", "deg", "60°x", Color(0xFFFFFFFF)}
|
||||||
|
for _, value := range failAngleValues {
|
||||||
|
if list.setAngleProperty("angle", value) {
|
||||||
|
t.Errorf(`setAngleProperty("angle", %v) success`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
colorValues := []interface{}{"@color", "#FF234567", "#234567", "rgba(30%, 128, 0.5, .25)", "rgb(30%, 128, 0.5)", Color(0xFFFFFFFF), 0xFFFFFFFF, White}
|
||||||
|
for _, color := range colorValues {
|
||||||
|
if !list.setColorProperty("color", color) {
|
||||||
|
t.Errorf(`list.setColorProperty("color", %v) fail`, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failColorValues := []interface{}{"@color|2", "#FF234567FF", "#TT234567", "rgba(500%, 128, 10.5, .25)", 10.6}
|
||||||
|
for _, color := range failColorValues {
|
||||||
|
if list.setColorProperty("color", color) {
|
||||||
|
t.Errorf(`list.setColorProperty("color", %v) success`, color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enumValues := []interface{}{"@enum", "inherit", "on", Inherit, 2}
|
||||||
|
inheritOffOn := inheritOffOnValues()
|
||||||
|
for _, value := range enumValues {
|
||||||
|
if !list.setEnumProperty("enum", value, inheritOffOn) {
|
||||||
|
t.Errorf(`list.setEnumProperty("enum", %v, %v) fail`, value, inheritOffOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failEnumValues := []interface{}{"@enum 13", "inherit2", "onn", -1, 10}
|
||||||
|
for _, value := range failEnumValues {
|
||||||
|
if list.setEnumProperty("enum", value, inheritOffOn) {
|
||||||
|
t.Errorf(`list.setEnumProperty("enum", %v, %v) success`, value, inheritOffOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolValues := []interface{}{"@bool", "true", "yes ", "on", " 1", "false", "no", "off", "0", 0, 1, false, true}
|
||||||
|
for _, value := range boolValues {
|
||||||
|
if !list.setBoolProperty("bool", value) {
|
||||||
|
t.Errorf(`list.setBoolProperty("bool", %v) fail`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failBoolValues := []interface{}{"@bool,2", "tr", "ys", "10", -1, 10, 0.8}
|
||||||
|
for _, value := range failBoolValues {
|
||||||
|
if list.setBoolProperty("bool", value) {
|
||||||
|
t.Errorf(`list.setBoolProperty("bool", %v) success`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
intValues := []interface{}{"@int", " 100", "-10 ", 0, 250}
|
||||||
|
for _, value := range intValues {
|
||||||
|
if !list.setIntProperty("int", value) {
|
||||||
|
t.Errorf(`list.setIntProperty("int", %v) fail`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failIntValues := []interface{}{"@int|10", "100i", "-1.0 ", 0.0}
|
||||||
|
for _, value := range failIntValues {
|
||||||
|
if list.setIntProperty("int", value) {
|
||||||
|
t.Errorf(`list.setIntProperty("int", %v) success`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
floatValues := []interface{}{"@float", " 100.25", "-1.5e12 ", uint(0), 250, float32(10.2), float64(0)}
|
||||||
|
for _, value := range floatValues {
|
||||||
|
if !list.setFloatProperty("float", value) {
|
||||||
|
t.Errorf(`list.setFloatProperty("float", %v) fail`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failFloatValues := []interface{}{"@float|2", " 100.25i", "-1.5ee12 ", "abc"}
|
||||||
|
for _, value := range failFloatValues {
|
||||||
|
if list.setFloatProperty("float", value) {
|
||||||
|
t.Errorf(`list.setFloatProperty("float", %v) success`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boundsValues := []interface{}{"@bounds", "10px,20pt,@bottom,0", Em(2), []interface{}{"@top", Px(10), AutoSize(), "14pt"}}
|
||||||
|
for _, value := range boundsValues {
|
||||||
|
if !list.setBoundsProperty("margin", value) {
|
||||||
|
t.Errorf(`list.setBoundsProperty("margin", %v) fail`, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,247 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func stringProperty(properties Properties, tag string, session Session) (string, bool) {
|
||||||
|
if value := properties.getRaw(tag); value != nil {
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
return session.resolveConstants(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToSizeUnit(value interface{}, session Session) (SizeUnit, bool) {
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case SizeUnit:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
return StringToSizeUnit(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AutoSize(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeProperty(properties Properties, tag string, session Session) (SizeUnit, bool) {
|
||||||
|
return valueToSizeUnit(properties.getRaw(tag), session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func angleProperty(properties Properties, tag string, session Session) (AngleUnit, bool) {
|
||||||
|
if value := properties.getRaw(tag); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case AngleUnit:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
return StringToAngleUnit(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AngleUnit{Type: 0, Value: 0}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToColor(value interface{}, session Session) (Color, bool) {
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case Color:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if len(value) > 1 && value[0] == '@' {
|
||||||
|
return session.Color(value[1:])
|
||||||
|
}
|
||||||
|
return StringToColor(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Color(0), false
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorProperty(properties Properties, tag string, session Session) (Color, bool) {
|
||||||
|
return valueToColor(properties.getRaw(tag), session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToEnum(value interface{}, tag string, session Session, defaultValue int) (int, bool) {
|
||||||
|
if value != nil {
|
||||||
|
values := enumProperties[tag].values
|
||||||
|
switch value := value.(type) {
|
||||||
|
case int:
|
||||||
|
if value >= 0 && value < len(values) {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
if tag == Orientation {
|
||||||
|
switch strings.ToLower(text) {
|
||||||
|
case "vertical":
|
||||||
|
value = "up-down"
|
||||||
|
|
||||||
|
case "horizontal":
|
||||||
|
value = "left-to-right"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result, ok := enumStringToInt(text, values, true); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func enumStringToInt(value string, enumValues []string, logError bool) (int, bool) {
|
||||||
|
value = strings.Trim(value, " \t\n\r")
|
||||||
|
|
||||||
|
for n, val := range enumValues {
|
||||||
|
if val == value {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, err := strconv.Atoi(value); err == nil {
|
||||||
|
if n >= 0 && n < len(enumValues) {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if logError {
|
||||||
|
ErrorLogF(`Out of bounds: value index = %d, valid values = [%v]`, n, enumValues)
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
value = strings.ToLower(value)
|
||||||
|
for n, val := range enumValues {
|
||||||
|
if val == value {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if logError {
|
||||||
|
ErrorLogF(`Unknown "%s" value. Valid values = [%v]`, value, enumValues)
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func enumProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) {
|
||||||
|
return valueToEnum(properties.getRaw(tag), tag, session, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToBool(value interface{}, session Session) (bool, bool) {
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case bool:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
switch strings.ToLower(text) {
|
||||||
|
case "true", "yes", "on", "1":
|
||||||
|
return true, true
|
||||||
|
|
||||||
|
case "false", "no", "off", "0":
|
||||||
|
return false, true
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLog(`The error of converting of "` + text + `" to bool`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolProperty(properties Properties, tag string, session Session) (bool, bool) {
|
||||||
|
return valueToBool(properties.getRaw(tag), session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToInt(value interface{}, session Session, defaultValue int) (int, bool) {
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
n, err := strconv.Atoi(strings.Trim(text, " \t"))
|
||||||
|
if err == nil {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
} else {
|
||||||
|
n, err := strconv.Atoi(strings.Trim(value, " \t"))
|
||||||
|
if err == nil {
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return isInt(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func intProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) {
|
||||||
|
return valueToInt(properties.getRaw(tag), session, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToFloat(value interface{}, session Session, defaultValue float64) (float64, bool) {
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
f, err := strconv.ParseFloat(text, 64)
|
||||||
|
if err == nil {
|
||||||
|
return f, true
|
||||||
|
}
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func floatProperty(properties Properties, tag string, session Session, defaultValue float64) (float64, bool) {
|
||||||
|
return valueToFloat(properties.getRaw(tag), session, defaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToRange(value interface{}, session Session) (Range, bool) {
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case Range:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case int:
|
||||||
|
return Range{First: value, Last: value}, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
var result Range
|
||||||
|
if result.setValue(text) {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Range{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func rangeProperty(properties Properties, tag string, session Session) (Range, bool) {
|
||||||
|
return valueToRange(properties.getRaw(tag), session)
|
||||||
|
}
|
|
@ -0,0 +1,449 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ID is the constant for the "id" property tag.
|
||||||
|
ID = "id"
|
||||||
|
// Style is the constant for the "style" property tag.
|
||||||
|
Style = "style"
|
||||||
|
// StyleDisabled is the constant for the "style-disabled" property tag.
|
||||||
|
StyleDisabled = "style-disabled"
|
||||||
|
// Disabled is the constant for the "disabled" property tag.
|
||||||
|
Disabled = "disabled"
|
||||||
|
// Semantics is the constant for the "semantics" property tag.
|
||||||
|
Semantics = "semantics"
|
||||||
|
// Visibility is the constant for the "visibility" property tag.
|
||||||
|
Visibility = "visibility"
|
||||||
|
// ZIndex is the constant for the "z-index" property tag.
|
||||||
|
// The int "z-index" property sets the z-order of a positioned view.
|
||||||
|
// Overlapping views with a larger z-index cover those with a smaller one.
|
||||||
|
ZIndex = "z-index"
|
||||||
|
// Opacity is the constant for the "opacity" property tag.
|
||||||
|
// The float "opacity" property in [1..0] range sets the opacity of an element.
|
||||||
|
// Opacity is the degree to which content behind an element is hidden, and is the opposite of transparency.
|
||||||
|
Opacity = "opacity"
|
||||||
|
// Row is the constant for the "row" property tag.
|
||||||
|
Row = "row"
|
||||||
|
// Column is the constant for the "column" property tag.
|
||||||
|
Column = "column"
|
||||||
|
// Left is the constant for the "left" property tag.
|
||||||
|
// The "left" SizeUnit property participates in specifying the left border position of a positioned view.
|
||||||
|
// Used only for views placed in an AbsoluteLayout.
|
||||||
|
Left = "left"
|
||||||
|
// Right is the constant for the "right" property tag.
|
||||||
|
// The "right" SizeUnit property participates in specifying the right border position of a positioned view.
|
||||||
|
// Used only for views placed in an AbsoluteLayout.
|
||||||
|
Right = "right"
|
||||||
|
// Top is the constant for the "top" property tag.
|
||||||
|
// The "top" SizeUnit property participates in specifying the top border position of a positioned view.
|
||||||
|
// Used only for views placed in an AbsoluteLayout.
|
||||||
|
Top = "top"
|
||||||
|
// Bottom is the constant for the "bottom" property tag.
|
||||||
|
// The "bottom" SizeUnit property participates in specifying the bottom border position of a positioned view.
|
||||||
|
// Used only for views placed in an AbsoluteLayout.
|
||||||
|
Bottom = "bottom"
|
||||||
|
// Width is the constant for the "width" property tag.
|
||||||
|
// The "width" SizeUnit property sets an view's width.
|
||||||
|
Width = "width"
|
||||||
|
// Height is the constant for the "height" property tag.
|
||||||
|
// The "height" SizeUnit property sets an view's height.
|
||||||
|
Height = "height"
|
||||||
|
// MinWidth is the constant for the "min-width" property tag.
|
||||||
|
// The "width" SizeUnit property sets an view's minimal width.
|
||||||
|
MinWidth = "min-width"
|
||||||
|
// MinHeight is the constant for the "min-height" property tag.
|
||||||
|
// The "height" SizeUnit property sets an view's minimal height.
|
||||||
|
MinHeight = "min-height"
|
||||||
|
// MaxWidth is the constant for the "max-width" property tag.
|
||||||
|
// The "width" SizeUnit property sets an view's maximal width.
|
||||||
|
MaxWidth = "max-width"
|
||||||
|
// MaxHeight is the constant for the "max-height" property tag.
|
||||||
|
// The "height" SizeUnit property sets an view's maximal height.
|
||||||
|
MaxHeight = "max-height"
|
||||||
|
// Margin is the constant for the "margin" property tag.
|
||||||
|
// The "margin" property sets the margin area on all four sides of an element.
|
||||||
|
// ...
|
||||||
|
Margin = "margin"
|
||||||
|
// MarginLeft is the constant for the "margin-left" property tag.
|
||||||
|
// The "margin-left" SizeUnit property sets the margin area on the left of a view.
|
||||||
|
// A positive value places it farther from its neighbors, while a negative value places it closer.
|
||||||
|
MarginLeft = "margin-left"
|
||||||
|
// MarginRight is the constant for the "margin-right" property tag.
|
||||||
|
// The "margin-right" SizeUnit property sets the margin area on the right of a view.
|
||||||
|
// A positive value places it farther from its neighbors, while a negative value places it closer.
|
||||||
|
MarginRight = "margin-right"
|
||||||
|
// MarginTop is the constant for the "margin-top" property tag.
|
||||||
|
// The "margin-top" SizeUnit property sets the margin area on the top of a view.
|
||||||
|
// A positive value places it farther from its neighbors, while a negative value places it closer.
|
||||||
|
MarginTop = "margin-top"
|
||||||
|
// MarginBottom is the constant for the "margin-bottom" property tag.
|
||||||
|
// The "margin-bottom" SizeUnit property sets the margin area on the bottom of a view.
|
||||||
|
// A positive value places it farther from its neighbors, while a negative value places it closer.
|
||||||
|
MarginBottom = "margin-bottom"
|
||||||
|
// Padding is the constant for the "padding" property tag.
|
||||||
|
// The "padding" Bounds property sets the padding area on all four sides of a view at once.
|
||||||
|
// An element's padding area is the space between its content and its border.
|
||||||
|
Padding = "padding"
|
||||||
|
// PaddingLeft is the constant for the "padding-left" property tag.
|
||||||
|
// The "padding-left" SizeUnit property sets the width of the padding area to the left of a view.
|
||||||
|
PaddingLeft = "padding-left"
|
||||||
|
// PaddingRight is the constant for the "padding-right" property tag.
|
||||||
|
// The "padding-right" SizeUnit property sets the width of the padding area to the right of a view.
|
||||||
|
PaddingRight = "padding-right"
|
||||||
|
// PaddingTop is the constant for the "padding-top" property tag.
|
||||||
|
// The "padding-top" SizeUnit property sets the height of the padding area to the top of a view.
|
||||||
|
PaddingTop = "padding-top"
|
||||||
|
// PaddingBottom is the constant for the "padding-bottom" property tag.
|
||||||
|
// The "padding-bottom" SizeUnit property sets the height of the padding area to the bottom of a view.
|
||||||
|
PaddingBottom = "padding-bottom"
|
||||||
|
// BackgroundColor is the constant for the "background-color" property tag.
|
||||||
|
// The "background-color" property sets the background color of a view.
|
||||||
|
BackgroundColor = "background-color"
|
||||||
|
// Background is the constant for the "background" property tag.
|
||||||
|
// The "background" property sets one or more background images and/or gradients on a view.
|
||||||
|
// ...
|
||||||
|
Background = "background"
|
||||||
|
// Cursor is the constant for the "cursor" property tag.
|
||||||
|
// The "cursor" int property sets the type of mouse cursor, if any, to show when the mouse pointer is over a view
|
||||||
|
// Valid values are "auto" (0), "default" (1), "none" (2), "context-menu" (3), "help" (4), "pointer" (5),
|
||||||
|
// "progress" (6), "wait" (7), "cell" (8), "crosshair" (9), "text" (10), "vertical-text" (11), "alias" (12),
|
||||||
|
// "copy" (13), "move" (14), "no-drop" (15), "not-allowed" (16), "e-resize" (17), "n-resize" (18),
|
||||||
|
// "ne-resize" (19), "nw-resize" (20), "s-resize" (21), "se-resize" (22), "sw-resize" (23), "w-resize" (24),
|
||||||
|
// "ew-resize" (25), "ns-resize" (26), "nesw-resize" (27), "nwse-resize" (28), "col-resize" (29),
|
||||||
|
// "row-resize" (30), "all-scroll" (31), "zoom-in" (32), "zoom-out" (33), "grab" (34), "grabbing" (35).
|
||||||
|
Cursor = "cursor"
|
||||||
|
// Border is the constant for the "border" property tag.
|
||||||
|
// The "border" property sets a view's border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
Border = "border"
|
||||||
|
// BorderLeft is the constant for the "border-left" property tag.
|
||||||
|
// The "border-left" property sets a view's left border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
BorderLeft = "border-left"
|
||||||
|
// BorderRight is the constant for the "border-right" property tag.
|
||||||
|
// The "border-right" property sets a view's right border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
BorderRight = "border-right"
|
||||||
|
// BorderTop is the constant for the "border-top" property tag.
|
||||||
|
// The "border-top" property sets a view's top border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
BorderTop = "border-top"
|
||||||
|
// BorderBottom is the constant for the "border-bottom" property tag.
|
||||||
|
// The "border-bottom" property sets a view's bottom border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
BorderBottom = "border-bottom"
|
||||||
|
// BorderStyle is the constant for the "border-style" property tag.
|
||||||
|
// The "border-style" property sets the line style for all four sides of a view's border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
BorderStyle = "border-style"
|
||||||
|
// BorderLeftStyle is the constant for the "border-left-style" property tag.
|
||||||
|
// The "border-left-style" int property sets the line style of a view's left border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
BorderLeftStyle = "border-left-style"
|
||||||
|
// BorderRightStyle is the constant for the "border-right-style" property tag.
|
||||||
|
// The "border-right-style" int property sets the line style of a view's right border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
BorderRightStyle = "border-right-style"
|
||||||
|
// BorderTopStyle is the constant for the "border-top-style" property tag.
|
||||||
|
// The "border-top-style" int property sets the line style of a view's top border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
BorderTopStyle = "border-top-style"
|
||||||
|
// BorderBottomStyle is the constant for the "border-bottom-style" property tag.
|
||||||
|
// The "border-bottom-style" int property sets the line style of a view's bottom border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
BorderBottomStyle = "border-bottom-style"
|
||||||
|
// BorderWidth is the constant for the "border-width" property tag.
|
||||||
|
// The "border-width" property sets the line width for all four sides of a view's border.
|
||||||
|
BorderWidth = "border-width"
|
||||||
|
// BorderLeftWidth is the constant for the "border-left-width" property tag.
|
||||||
|
// The "border-left-width" SizeUnit property sets the line width of a view's left border.
|
||||||
|
BorderLeftWidth = "border-left-width"
|
||||||
|
// BorderRightWidth is the constant for the "border-right-width" property tag.
|
||||||
|
// The "border-right-width" SizeUnit property sets the line width of a view's right border.
|
||||||
|
BorderRightWidth = "border-right-width"
|
||||||
|
// BorderTopWidth is the constant for the "border-top-width" property tag.
|
||||||
|
// The "border-top-width" SizeUnit property sets the line width of a view's top border.
|
||||||
|
BorderTopWidth = "border-top-width"
|
||||||
|
// BorderBottomWidth is the constant for the "border-bottom-width" property tag.
|
||||||
|
// The "border-bottom-width" SizeUnit property sets the line width of a view's bottom border.
|
||||||
|
BorderBottomWidth = "border-bottom-width"
|
||||||
|
// BorderColor is the constant for the "border-color" property tag.
|
||||||
|
// The "border-color" property sets the line color for all four sides of a view's border.
|
||||||
|
BorderColor = "border-color"
|
||||||
|
// BorderLeftColor is the constant for the "border-left-color" property tag.
|
||||||
|
// The "border-left-color" property sets the line color of a view's left border.
|
||||||
|
BorderLeftColor = "border-left-color"
|
||||||
|
// BorderRightColor is the constant for the "border-right-color" property tag.
|
||||||
|
// The "border-right-color" property sets the line color of a view's right border.
|
||||||
|
BorderRightColor = "border-right-color"
|
||||||
|
// BorderTopColor is the constant for the "border-top-color" property tag.
|
||||||
|
// The "border-top-color" property sets the line color of a view's top border.
|
||||||
|
BorderTopColor = "border-top-color"
|
||||||
|
// BorderBottomColor is the constant for the "border-bottom-color" property tag.
|
||||||
|
// The "border-bottom-color" property sets the line color of a view's bottom border.
|
||||||
|
BorderBottomColor = "border-bottom-color"
|
||||||
|
// Outline is the constant for the "outline" property tag.
|
||||||
|
// The "border" property sets a view's outline. It sets the values of an outline width, style, and color.
|
||||||
|
Outline = "outline"
|
||||||
|
// OutlineStyle is the constant for the "outline-style" property tag.
|
||||||
|
// The "outline-style" int property sets the style of an view's outline.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
OutlineStyle = "outline-style"
|
||||||
|
// OutlineColor is the constant for the "outline-color" property tag.
|
||||||
|
// The "outline-color" property sets the color of an view's outline.
|
||||||
|
OutlineColor = "outline-color"
|
||||||
|
// OutlineWidth is the constant for the "outline-width" property tag.
|
||||||
|
// The "outline-width" SizeUnit property sets the width of an view's outline.
|
||||||
|
OutlineWidth = "outline-width"
|
||||||
|
// Shadow is the constant for the "shadow" property tag.
|
||||||
|
// The "shadow" property adds shadow effects around a view's frame. A shadow is described
|
||||||
|
// by X and Y offsets relative to the element, blur and spread radius, and color.
|
||||||
|
// ...
|
||||||
|
Shadow = "shadow"
|
||||||
|
// FontName is the constant for the "font-name" property tag.
|
||||||
|
// The "font-name" string property specifies a prioritized list of one or more font family names and/or
|
||||||
|
// generic family names for the selected view. Values are separated by commas to indicate that they are alternatives.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
FontName = "font-name"
|
||||||
|
// TextColor is the constant for the "text-color" property tag.
|
||||||
|
// The "color" property sets the foreground color value of a view's text and text decorations.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextColor = "text-color"
|
||||||
|
// TextSize is the constant for the "text-size" property tag.
|
||||||
|
// The "text-size" SizeUnit property sets the size of the font.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextSize = "text-size"
|
||||||
|
// Italic is the constant for the "italic" property tag.
|
||||||
|
// The "italic" is the bool property. If it is "true" then a text is displayed in italics.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
Italic = "italic"
|
||||||
|
// SmallCaps is the constant for the "small-caps" property tag.
|
||||||
|
// The "small-caps" is the bool property. If it is "true" then a text is displayed in small caps.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
SmallCaps = "small-caps"
|
||||||
|
// Strikethrough is the constant for the "strikethrough" property tag.
|
||||||
|
// The "strikethrough" is the bool property. If it is "true" then a text is displayed strikethrough.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
Strikethrough = "strikethrough"
|
||||||
|
// Overline is the constant for the "overline" property tag.
|
||||||
|
// The "overline" is the bool property. If it is "true" then a text is displayed overlined.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
Overline = "overline"
|
||||||
|
// Underline is the constant for the "underline" property tag.
|
||||||
|
// The "underline" is the bool property. If it is "true" then a text is displayed underlined.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
Underline = "underline"
|
||||||
|
// TextLineThickness is the constant for the "text-decoration-thickness" property tag.
|
||||||
|
// The "text-decoration-thickness" SizeUnit property sets the stroke thickness of the decoration line that
|
||||||
|
// is used on text in an element, such as a line-through, underline, or overline.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextLineThickness = "text-line-thickness"
|
||||||
|
// TextLineStyle is the constant for the "text-decoration-style" property tag.
|
||||||
|
// The "text-decoration-style" int property sets the style of the lines specified by "text-decoration" property.
|
||||||
|
// The style applies to all lines that are set with "text-decoration" property.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextLineStyle = "text-line-style"
|
||||||
|
// TextLineColor is the constant for the "text-decoration-color" property tag.
|
||||||
|
// The "text-decoration-color" Color property sets the color of the lines specified by "text-decoration" property.
|
||||||
|
// The color applies to all lines that are set with "text-decoration" property.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextLineColor = "text-line-color"
|
||||||
|
// TextWeight is the constant for the "text-weight" property tag.
|
||||||
|
// Valid values are SolidLine (1), DashedLine (2), DottedLine (3), DoubleLine (4) and WavyLine (5).
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextWeight = "text-weight"
|
||||||
|
// TextAlign is the constant for the "text-align" property tag.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextAlign = "text-align"
|
||||||
|
// TextIndent is the constant for the "text-indent" property tag.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextIndent = "text-indent"
|
||||||
|
// TextShadow is the constant for the "text-shadow" property tag.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextShadow = "text-shadow"
|
||||||
|
// LetterSpacing is the constant for the "letter-spacing" property tag.
|
||||||
|
// The "letter-spacing" SizeUnit property sets the horizontal spacing behavior between text characters.
|
||||||
|
// This value is added to the natural spacing between characters while rendering the text.
|
||||||
|
// Positive values of letter-spacing causes characters to spread farther apart,
|
||||||
|
// while negative values of letter-spacing bring characters closer together.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
LetterSpacing = "letter-spacing"
|
||||||
|
// WordSpacing is the constant for the "word-spacing" property tag.
|
||||||
|
// The "word-spacing" SizeUnit property sets the length of space between words and between tags.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
WordSpacing = "word-spacing"
|
||||||
|
// LineHeight is the constant for the "line-height" property tag.
|
||||||
|
// The "line-height" SizeUnit property sets the height of a line box.
|
||||||
|
// It's commonly used to set the distance between lines of text.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
LineHeight = "line-height"
|
||||||
|
// WhiteSpace is the constant for the "white-space" property tag.
|
||||||
|
// The "white-space" int property sets how white space inside an element is handled.
|
||||||
|
// Valid values are WhiteSpaceNormal (0), WhiteSpaceNowrap (1), WhiteSpacePre (2),
|
||||||
|
// WhiteSpacePreWrap (3), WhiteSpacePreLine (4), WhiteSpaceBreakSpaces (5)
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
WhiteSpace = "white-space"
|
||||||
|
// WordBreak is the constant for the "word-break" property tag.
|
||||||
|
// The "word-break" int property sets whether line breaks appear wherever the text would otherwise overflow its content box.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
WordBreak = "word-break"
|
||||||
|
// TextTransform is the constant for the "text-transform" property tag.
|
||||||
|
// The "text-transform" int property specifies how to capitalize an element's text.
|
||||||
|
// It can be used to make text appear in all-uppercase or all-lowercase, or with each word capitalized.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextTransform = "text-transform"
|
||||||
|
// TextDirection is the constant for the "text-direction" property tag.
|
||||||
|
// The "text-direction" int property sets the direction of text, table columns, and horizontal overflow.
|
||||||
|
// Use 1 (LeftToRightDirection) for languages written from right to left (like Hebrew or Arabic),
|
||||||
|
// and 2 (RightToLeftDirection) for those written from left to right (like English and most other languages).
|
||||||
|
// The default value of the property is 0 (SystemTextDirection): use the system text direction.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
TextDirection = "text-direction"
|
||||||
|
// WritingMode is the constant for the "writing-mode" property tag.
|
||||||
|
// The "writing-mode" int property sets whether lines of text are laid out horizontally or vertically,
|
||||||
|
// as well as the direction in which blocks progress
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
WritingMode = "writing-mode"
|
||||||
|
// VerticalTextOrientation is the constant for the "vertical-text-orientation" property tag.
|
||||||
|
// The "vertical-text-orientation" int property sets the orientation of the text characters in a line.
|
||||||
|
// It only affects text in vertical mode ("writing-mode" property).
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
VerticalTextOrientation = "vertical-text-orientation"
|
||||||
|
// TextTverflow is the constant for the "text-overflow" property tag.
|
||||||
|
// The "text-overflow" int property sets how hidden overflow content is signaled to users.
|
||||||
|
// It can be clipped or display an ellipsis ('…'). Valid values are
|
||||||
|
TextOverflow = "text-overflow"
|
||||||
|
// Hint is the constant for the "hint" property tag.
|
||||||
|
// The "hint" string property sets a hint to the user of what can be entered in the control.
|
||||||
|
Hint = "hint"
|
||||||
|
// MaxLength is the constant for the "max-length" property tag.
|
||||||
|
// The "max-length" int property sets the maximum number of characters that the user can enter
|
||||||
|
MaxLength = "max-length"
|
||||||
|
// ReadOnly is the constant for the "readonly" property tag.
|
||||||
|
// This bool property indicates that the user cannot modify the value of the EditView.
|
||||||
|
ReadOnly = "readonly"
|
||||||
|
// Content is the constant for the "content" property tag.
|
||||||
|
Content = "content"
|
||||||
|
// Items is the constant for the "items" property tag.
|
||||||
|
Items = "items"
|
||||||
|
// Current is the constant for the "current" property tag.
|
||||||
|
Current = "current"
|
||||||
|
// Type is the constant for the "type" property tag.
|
||||||
|
Type = "type"
|
||||||
|
// Pattern is the constant for the "pattern" property tag.
|
||||||
|
Pattern = "pattern"
|
||||||
|
// CellWidth is the constant for the "cell-width" property tag.
|
||||||
|
CellWidth = "cell-width"
|
||||||
|
// CellHeight is the constant for the "cell-height" property tag.
|
||||||
|
CellHeight = "cell-height"
|
||||||
|
// RowGap is the constant for the "row-gap" property tag.
|
||||||
|
GridRowGap = "grid-row-gap"
|
||||||
|
// ColumnGap is the constant for the "column-gap" property tag.
|
||||||
|
GridColumnGap = "grid-column-gap"
|
||||||
|
// Source is the constant for the "src" property tag.
|
||||||
|
Source = "src"
|
||||||
|
// Fit is the constant for the "fit" property tag.
|
||||||
|
Fit = "fit"
|
||||||
|
backgroundFit = "background-fit"
|
||||||
|
// Repeat is the constant for the "repeat" property tag.
|
||||||
|
Repeat = "repeat"
|
||||||
|
// Attachment is the constant for the "attachment" property tag.
|
||||||
|
Attachment = "attachment"
|
||||||
|
// Clip is the constant for the "clip" property tag.
|
||||||
|
BackgroundClip = "background-clip"
|
||||||
|
// Gradient is the constant for the "gradient" property tag.
|
||||||
|
Gradient = "gradient"
|
||||||
|
// Direction is the constant for the "direction" property tag.
|
||||||
|
Direction = "direction"
|
||||||
|
// Repeating is the constant for the "repeating" property tag.
|
||||||
|
Repeating = "repeating"
|
||||||
|
// RadialGradientRadius is the constant for the "radial-gradient-radius" property tag.
|
||||||
|
RadialGradientRadius = "radial-gradient-radius"
|
||||||
|
// RadialGradientShape is the constant for the "radial-gradient-shape" property tag.
|
||||||
|
RadialGradientShape = "radial-gradient-shape"
|
||||||
|
// Shape is the constant for the "shape" property tag. It's a short form of "radial-gradient-shape"
|
||||||
|
Shape = "shape"
|
||||||
|
// CenterX is the constant for the "center-x" property tag.
|
||||||
|
CenterX = "center-x"
|
||||||
|
// CenterY is the constant for the "center-x" property tag.
|
||||||
|
CenterY = "center-y"
|
||||||
|
// AltText is the constant for the "alt-text" property tag.
|
||||||
|
AltText = "alt-text"
|
||||||
|
altProperty = "alt"
|
||||||
|
// AvoidBreak is the constant for the "avoid-break" property tag.
|
||||||
|
// The "avoid-break" bool property sets how region breaks should behave inside a generated box.
|
||||||
|
// If the property value is "true" then fvoids any break from being inserted within the principal box.
|
||||||
|
// If the property value is "false" then allows, but does not force, any break to be inserted within
|
||||||
|
// the principal box.
|
||||||
|
AvoidBreak = "avoid-break"
|
||||||
|
// ItemWidth is the constant for the "item-width" property tag.
|
||||||
|
ItemWidth = "item-width"
|
||||||
|
// ItemHeight is the constant for the "item-height" property tag.
|
||||||
|
ItemHeight = "item-height"
|
||||||
|
// Wrap is the constant for the "wrap" property tag.
|
||||||
|
Wrap = "wrap"
|
||||||
|
// Min is the constant for the "min" property tag.
|
||||||
|
Min = "min"
|
||||||
|
// Max is the constant for the "max" property tag.
|
||||||
|
Max = "max"
|
||||||
|
// Step is the constant for the "step" property tag.
|
||||||
|
Step = "step"
|
||||||
|
// Value is the constant for the "value" property tag.
|
||||||
|
Value = "value"
|
||||||
|
// Orientation is the constant for the "orientation" property tag.
|
||||||
|
Orientation = "orientation"
|
||||||
|
// Anchor is the constant for the "anchor" property tag.
|
||||||
|
Anchor = "anchor"
|
||||||
|
// Gap is the constant for the "gap" property tag.
|
||||||
|
Gap = "gap"
|
||||||
|
// Tabs is the constant for the "tabs" property tag.
|
||||||
|
Tabs = "tabs"
|
||||||
|
// TabStyle is the constant for the "tab-style" property tag.
|
||||||
|
TabStyle = "tab-style"
|
||||||
|
// CurrentTabStyle is the constant for the "current-tab-style" property tag.
|
||||||
|
CurrentTabStyle = "current-tab-style"
|
||||||
|
// Text is the constant for the "text" property tag.
|
||||||
|
Text = "text"
|
||||||
|
// VerticalAlign is the constant for the "vertical-align" property tag.
|
||||||
|
VerticalAlign = "vertical-align"
|
||||||
|
// HorizontalAlign is the constant for the "horizontal-align" property tag.
|
||||||
|
// The "horizontal-align" int property sets the horizontal alignment of the content inside a block element
|
||||||
|
HorizontalAlign = "horizontal-align"
|
||||||
|
// ImageVerticalAlign is the constant for the "image-vertical-align" property tag.
|
||||||
|
ImageVerticalAlign = "image-vertical-align"
|
||||||
|
// ImageHorizontalAlign is the constant for the "image-horizontal-align" property tag.
|
||||||
|
ImageHorizontalAlign = "image-horizontal-align"
|
||||||
|
// Checked is the constant for the "checked" property tag.
|
||||||
|
Checked = "checked"
|
||||||
|
// ItemVerticalAlign is the constant for the "item-vertical-align" property tag.
|
||||||
|
ItemVerticalAlign = "item-vertical-align"
|
||||||
|
// ItemHorizontalAlign is the constant for the "item-horizontal-align" property tag.
|
||||||
|
ItemHorizontalAlign = "item-horizontal-align"
|
||||||
|
// ItemCheckbox is the constant for the "checkbox" property tag.
|
||||||
|
ItemCheckbox = "checkbox"
|
||||||
|
// CheckboxHorizontalAlign is the constant for the "checkbox-horizontal-align" property tag.
|
||||||
|
CheckboxHorizontalAlign = "checkbox-horizontal-align"
|
||||||
|
// CheckboxVerticalAlign is the constant for the "checkbox-vertical-align" property tag.
|
||||||
|
CheckboxVerticalAlign = "checkbox-vertical-align"
|
||||||
|
// NotTranslate is the constant for the "not-translate" property tag.
|
||||||
|
// This bool property indicates that no need to translate the text.
|
||||||
|
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
|
||||||
|
NotTranslate = "not-translate"
|
||||||
|
// Filter is the constant for the "filter" property tag.
|
||||||
|
// The "filter" property applies graphical effects like blur or color shift to a View.
|
||||||
|
Filter = "filter"
|
||||||
|
// Clip is the constant for the "clip" property tag.
|
||||||
|
// The "clip" property creates a clipping region that sets what part of a View should be shown.
|
||||||
|
Clip = "clip"
|
||||||
|
// Points is the constant for the "points" property tag.
|
||||||
|
Points = "points"
|
||||||
|
// ShapeOutside is the constant for the "shape-outside" property tag.
|
||||||
|
// The "shape-outside" property defines a shape (which may be non-rectangular) around which adjacent
|
||||||
|
// inline content should wrap. By default, inline content wraps around its margin box;
|
||||||
|
// "shape-outside" provides a way to customize this wrapping, making it possible to wrap text around
|
||||||
|
// complex objects rather than simple boxes.
|
||||||
|
ShapeOutside = "shape-outside"
|
||||||
|
// Float is the constant for the "float" property tag.
|
||||||
|
// The "float" property places a View on the left or right side of its container,
|
||||||
|
// allowing text and inline Views to wrap around it.
|
||||||
|
Float = "float"
|
||||||
|
)
|
|
@ -0,0 +1,764 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var colorProperties = []string{
|
||||||
|
ColorProperty,
|
||||||
|
BackgroundColor,
|
||||||
|
TextColor,
|
||||||
|
BorderColor,
|
||||||
|
BorderLeftColor,
|
||||||
|
BorderRightColor,
|
||||||
|
BorderTopColor,
|
||||||
|
BorderBottomColor,
|
||||||
|
OutlineColor,
|
||||||
|
TextLineColor,
|
||||||
|
ColorPickerValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPropertyInList(tag string, list []string) bool {
|
||||||
|
for _, prop := range list {
|
||||||
|
if prop == tag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var angleProperties = []string{
|
||||||
|
Rotate,
|
||||||
|
SkewX,
|
||||||
|
SkewY,
|
||||||
|
}
|
||||||
|
|
||||||
|
var boolProperties = []string{
|
||||||
|
Disabled,
|
||||||
|
Inset,
|
||||||
|
BackfaceVisible,
|
||||||
|
ReadOnly,
|
||||||
|
Spellcheck,
|
||||||
|
CloseButton,
|
||||||
|
OutsideClose,
|
||||||
|
Italic,
|
||||||
|
SmallCaps,
|
||||||
|
Strikethrough,
|
||||||
|
Overline,
|
||||||
|
Underline,
|
||||||
|
Expanded,
|
||||||
|
AvoidBreak,
|
||||||
|
NotTranslate,
|
||||||
|
Controls,
|
||||||
|
Loop,
|
||||||
|
Muted,
|
||||||
|
}
|
||||||
|
|
||||||
|
var intProperties = []string{
|
||||||
|
ZIndex,
|
||||||
|
HeadHeight,
|
||||||
|
FootHeight,
|
||||||
|
RowSpan,
|
||||||
|
ColumnSpan,
|
||||||
|
}
|
||||||
|
|
||||||
|
var floatProperties = map[string]struct{ min, max float64 }{
|
||||||
|
ScaleX: {min: -math.MaxFloat64, max: math.MaxFloat64},
|
||||||
|
ScaleY: {min: -math.MaxFloat64, max: math.MaxFloat64},
|
||||||
|
ScaleZ: {min: -math.MaxFloat64, max: math.MaxFloat64},
|
||||||
|
RotateX: {min: 0, max: 1},
|
||||||
|
RotateY: {min: 0, max: 1},
|
||||||
|
RotateZ: {min: 0, max: 1},
|
||||||
|
NumberPickerMax: {min: -math.MaxFloat64, max: math.MaxFloat64},
|
||||||
|
NumberPickerMin: {min: -math.MaxFloat64, max: math.MaxFloat64},
|
||||||
|
NumberPickerStep: {min: -math.MaxFloat64, max: math.MaxFloat64},
|
||||||
|
NumberPickerValue: {min: -math.MaxFloat64, max: math.MaxFloat64},
|
||||||
|
ProgressBarMax: {min: 0, max: math.MaxFloat64},
|
||||||
|
ProgressBarValue: {min: 0, max: math.MaxFloat64},
|
||||||
|
VideoWidth: {min: 0, max: 10000},
|
||||||
|
VideoHeight: {min: 0, max: 10000},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeProperties = map[string]string{
|
||||||
|
Width: Width,
|
||||||
|
Height: Height,
|
||||||
|
MinWidth: MinWidth,
|
||||||
|
MinHeight: MinHeight,
|
||||||
|
MaxWidth: MaxWidth,
|
||||||
|
MaxHeight: MaxHeight,
|
||||||
|
Left: Left,
|
||||||
|
Right: Right,
|
||||||
|
Top: Top,
|
||||||
|
Bottom: Bottom,
|
||||||
|
TextSize: "font-size",
|
||||||
|
TextIndent: TextIndent,
|
||||||
|
LetterSpacing: LetterSpacing,
|
||||||
|
WordSpacing: WordSpacing,
|
||||||
|
LineHeight: LineHeight,
|
||||||
|
TextLineThickness: "text-decoration-thickness",
|
||||||
|
GridRowGap: GridRowGap,
|
||||||
|
GridColumnGap: GridColumnGap,
|
||||||
|
ColumnWidth: ColumnWidth,
|
||||||
|
ColumnGap: ColumnGap,
|
||||||
|
Gap: Gap,
|
||||||
|
Margin: Margin,
|
||||||
|
MarginLeft: MarginLeft,
|
||||||
|
MarginRight: MarginRight,
|
||||||
|
MarginTop: MarginTop,
|
||||||
|
MarginBottom: MarginBottom,
|
||||||
|
Padding: Padding,
|
||||||
|
PaddingLeft: PaddingLeft,
|
||||||
|
PaddingRight: PaddingRight,
|
||||||
|
PaddingTop: PaddingTop,
|
||||||
|
PaddingBottom: PaddingBottom,
|
||||||
|
BorderWidth: BorderWidth,
|
||||||
|
BorderLeftWidth: BorderLeftWidth,
|
||||||
|
BorderRightWidth: BorderRightWidth,
|
||||||
|
BorderTopWidth: BorderTopWidth,
|
||||||
|
BorderBottomWidth: BorderBottomWidth,
|
||||||
|
OutlineWidth: OutlineWidth,
|
||||||
|
XOffset: XOffset,
|
||||||
|
YOffset: YOffset,
|
||||||
|
BlurRadius: BlurRadius,
|
||||||
|
SpreadRadius: SpreadRadius,
|
||||||
|
Perspective: Perspective,
|
||||||
|
PerspectiveOriginX: PerspectiveOriginX,
|
||||||
|
PerspectiveOriginY: PerspectiveOriginY,
|
||||||
|
OriginX: OriginX,
|
||||||
|
OriginY: OriginY,
|
||||||
|
OriginZ: OriginZ,
|
||||||
|
TranslateX: TranslateX,
|
||||||
|
TranslateY: TranslateY,
|
||||||
|
TranslateZ: TranslateZ,
|
||||||
|
Radius: Radius,
|
||||||
|
RadiusX: RadiusX,
|
||||||
|
RadiusY: RadiusY,
|
||||||
|
RadiusTopLeft: RadiusTopLeft,
|
||||||
|
RadiusTopLeftX: RadiusTopLeftX,
|
||||||
|
RadiusTopLeftY: RadiusTopLeftY,
|
||||||
|
RadiusTopRight: RadiusTopRight,
|
||||||
|
RadiusTopRightX: RadiusTopRightX,
|
||||||
|
RadiusTopRightY: RadiusTopRightY,
|
||||||
|
RadiusBottomLeft: RadiusBottomLeft,
|
||||||
|
RadiusBottomLeftX: RadiusBottomLeftX,
|
||||||
|
RadiusBottomLeftY: RadiusBottomLeftY,
|
||||||
|
RadiusBottomRight: RadiusBottomRight,
|
||||||
|
RadiusBottomRightX: RadiusBottomRightX,
|
||||||
|
RadiusBottomRightY: RadiusBottomRightY,
|
||||||
|
ItemWidth: ItemWidth,
|
||||||
|
ItemHeight: ItemHeight,
|
||||||
|
CenterX: CenterX,
|
||||||
|
CenterY: CenterX,
|
||||||
|
}
|
||||||
|
|
||||||
|
var enumProperties = map[string]struct {
|
||||||
|
values []string
|
||||||
|
cssTag string
|
||||||
|
cssValues []string
|
||||||
|
}{
|
||||||
|
Semantics: {
|
||||||
|
[]string{"default", "article", "section", "aside", "header", "main", "footer", "navigation", "figure", "figure-caption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"},
|
||||||
|
"",
|
||||||
|
[]string{"div", "article", "section", "aside", "header", "main", "footer", "nav", "figure", "figcaption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"},
|
||||||
|
},
|
||||||
|
Visibility: {
|
||||||
|
[]string{"visible", "invisible", "gone"},
|
||||||
|
Visibility,
|
||||||
|
[]string{"visible", "invisible", "gone"},
|
||||||
|
},
|
||||||
|
TextAlign: {
|
||||||
|
[]string{"left", "right", "center", "justify"},
|
||||||
|
TextAlign,
|
||||||
|
[]string{"left", "right", "center", "justify"},
|
||||||
|
},
|
||||||
|
TextTransform: {
|
||||||
|
[]string{"none", "capitalize", "lowercase", "uppercase"},
|
||||||
|
TextTransform,
|
||||||
|
[]string{"none", "capitalize", "lowercase", "uppercase"},
|
||||||
|
},
|
||||||
|
TextWeight: {
|
||||||
|
[]string{"inherit", "thin", "extra-light", "light", "normal", "medium", "semi-bold", "bold", "extra-bold", "black"},
|
||||||
|
"font-weight",
|
||||||
|
[]string{"inherit", "100", "200", "300", "normal", "500", "600", "bold", "800", "900"},
|
||||||
|
},
|
||||||
|
WhiteSpace: {
|
||||||
|
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
|
||||||
|
WhiteSpace,
|
||||||
|
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
|
||||||
|
},
|
||||||
|
WordBreak: {
|
||||||
|
[]string{"normal", "break-all", "keep-all", "break-word"},
|
||||||
|
WordBreak,
|
||||||
|
[]string{"normal", "break-all", "keep-all", "break-word"},
|
||||||
|
},
|
||||||
|
TextOverflow: {
|
||||||
|
[]string{"clip", "ellipsis"},
|
||||||
|
TextOverflow,
|
||||||
|
[]string{"clip", "ellipsis"},
|
||||||
|
},
|
||||||
|
WritingMode: {
|
||||||
|
[]string{"horizontal-top-to-bottom", "horizontal-bottom-to-top", "vertical-right-to-left", "vertical-left-to-right"},
|
||||||
|
WritingMode,
|
||||||
|
[]string{"horizontal-tb", "horizontal-bt", "vertical-rl", "vertical-lr"},
|
||||||
|
},
|
||||||
|
TextDirection: {
|
||||||
|
[]string{"system", "left-to-right", "right-to-left"},
|
||||||
|
"direction",
|
||||||
|
[]string{"", "ltr", "rtl"},
|
||||||
|
},
|
||||||
|
VerticalTextOrientation: {
|
||||||
|
[]string{"mixed", "upright"},
|
||||||
|
"text-orientation",
|
||||||
|
[]string{"mixed", "upright"},
|
||||||
|
},
|
||||||
|
TextLineStyle: {
|
||||||
|
[]string{"inherit", "solid", "dashed", "dotted", "double", "wavy"},
|
||||||
|
"text-decoration-style",
|
||||||
|
[]string{"inherit", "solid", "dashed", "dotted", "double", "wavy"},
|
||||||
|
},
|
||||||
|
BorderStyle: {
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
BorderStyle,
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
},
|
||||||
|
TopStyle: {
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
"",
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
},
|
||||||
|
RightStyle: {
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
"",
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
},
|
||||||
|
BottomStyle: {
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
"",
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
},
|
||||||
|
LeftStyle: {
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
"",
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
},
|
||||||
|
OutlineStyle: {
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
OutlineStyle,
|
||||||
|
[]string{"none", "solid", "dashed", "dotted", "double"},
|
||||||
|
},
|
||||||
|
Tabs: {
|
||||||
|
[]string{"hidden", "top", "bottom", "left", "right", "left-list", "right-list"},
|
||||||
|
"",
|
||||||
|
[]string{"hidden", "top", "bottom", "left", "right", "left-list", "right-list"},
|
||||||
|
},
|
||||||
|
NumberPickerType: {
|
||||||
|
[]string{"editor", "slider"},
|
||||||
|
"",
|
||||||
|
[]string{"editor", "slider"},
|
||||||
|
},
|
||||||
|
EditViewType: {
|
||||||
|
[]string{"text", "password", "email", "emails", "url", "phone", "multiline"},
|
||||||
|
"",
|
||||||
|
[]string{"text", "password", "email", "emails", "url", "phone", "multiline"},
|
||||||
|
},
|
||||||
|
Orientation: {
|
||||||
|
[]string{"up-down", "start-to-end", "bottom-up", "end-to-start"},
|
||||||
|
"",
|
||||||
|
[]string{"column", "row", "column-reverse", "row-reverse"},
|
||||||
|
},
|
||||||
|
Wrap: {
|
||||||
|
[]string{"off", "on", "reverse"},
|
||||||
|
"",
|
||||||
|
[]string{"nowrap", "wrap", "wrap-reverse"},
|
||||||
|
},
|
||||||
|
"list-orientation": {
|
||||||
|
[]string{"vertical", "horizontal"},
|
||||||
|
"",
|
||||||
|
[]string{"vertical", "horizontal"},
|
||||||
|
},
|
||||||
|
VerticalAlign: {
|
||||||
|
[]string{"top", "bottom", "center", "stretch"},
|
||||||
|
"",
|
||||||
|
[]string{"top", "bottom", "center", "stretch"},
|
||||||
|
},
|
||||||
|
HorizontalAlign: {
|
||||||
|
[]string{"left", "right", "center", "stretch"},
|
||||||
|
"",
|
||||||
|
[]string{"left", "right", "center", "stretch"},
|
||||||
|
},
|
||||||
|
ButtonsAlign: {
|
||||||
|
[]string{"left", "right", "center", "stretch"},
|
||||||
|
"",
|
||||||
|
[]string{"left", "right", "center", "stretch"},
|
||||||
|
},
|
||||||
|
CellVerticalAlign: {
|
||||||
|
[]string{"top", "bottom", "center", "stretch"},
|
||||||
|
"align-items",
|
||||||
|
[]string{"start", "end", "center", "stretch"},
|
||||||
|
},
|
||||||
|
CellHorizontalAlign: {
|
||||||
|
[]string{"left", "right", "center", "stretch"},
|
||||||
|
"justify-items",
|
||||||
|
[]string{"start", "end", "center", "stretch"},
|
||||||
|
},
|
||||||
|
ImageVerticalAlign: {
|
||||||
|
[]string{"top", "bottom", "center"},
|
||||||
|
"",
|
||||||
|
[]string{"top", "bottom", "center"},
|
||||||
|
},
|
||||||
|
ImageHorizontalAlign: {
|
||||||
|
[]string{"left", "right", "center"},
|
||||||
|
"",
|
||||||
|
[]string{"left", "right", "center"},
|
||||||
|
},
|
||||||
|
ItemVerticalAlign: {
|
||||||
|
[]string{"top", "bottom", "center", "stretch"},
|
||||||
|
"",
|
||||||
|
[]string{"start", "end", "center", "stretch"},
|
||||||
|
},
|
||||||
|
ItemHorizontalAlign: {
|
||||||
|
[]string{"left", "right", "center", "stretch"},
|
||||||
|
"",
|
||||||
|
[]string{"start", "end", "center", "stretch"},
|
||||||
|
},
|
||||||
|
CheckboxVerticalAlign: {
|
||||||
|
[]string{"top", "bottom", "center"},
|
||||||
|
"",
|
||||||
|
[]string{"start", "end", "center"},
|
||||||
|
},
|
||||||
|
CheckboxHorizontalAlign: {
|
||||||
|
[]string{"left", "right", "center"},
|
||||||
|
"",
|
||||||
|
[]string{"start", "end", "center"},
|
||||||
|
},
|
||||||
|
TableVerticalAlign: {
|
||||||
|
[]string{"top", "bottom", "center", "stretch", "baseline"},
|
||||||
|
"vertical-align",
|
||||||
|
[]string{"top", "bottom", "middle", "baseline", "baseline"},
|
||||||
|
},
|
||||||
|
Anchor: {
|
||||||
|
[]string{"top", "bottom"},
|
||||||
|
"",
|
||||||
|
[]string{"top", "bottom"},
|
||||||
|
},
|
||||||
|
Cursor: {
|
||||||
|
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
|
||||||
|
Cursor,
|
||||||
|
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
|
||||||
|
},
|
||||||
|
Fit: {
|
||||||
|
[]string{"none", "contain", "cover", "fill", "scale-down"},
|
||||||
|
"object-fit",
|
||||||
|
[]string{"none", "contain", "cover", "fill", "scale-down"},
|
||||||
|
},
|
||||||
|
backgroundFit: {
|
||||||
|
[]string{"none", "contain", "cover"},
|
||||||
|
"",
|
||||||
|
[]string{"none", "contain", "cover"},
|
||||||
|
},
|
||||||
|
Repeat: {
|
||||||
|
[]string{"no-repeat", "repeat", "repeat-x", "repeat-y", "round", "space"},
|
||||||
|
"",
|
||||||
|
[]string{"no-repeat", "repeat", "repeat-x", "repeat-y", "round", "space"},
|
||||||
|
},
|
||||||
|
Attachment: {
|
||||||
|
[]string{"scroll", "fixed", "local"},
|
||||||
|
"",
|
||||||
|
[]string{"scroll", "fixed", "local"},
|
||||||
|
},
|
||||||
|
BackgroundClip: {
|
||||||
|
[]string{"border-box", "padding-box", "content-box"}, // "text"},
|
||||||
|
"background-clip",
|
||||||
|
[]string{"border-box", "padding-box", "content-box"}, // "text"},
|
||||||
|
},
|
||||||
|
Direction: {
|
||||||
|
[]string{"to-top", "to-right-top", "to-right", "to-right-bottom", "to-bottom", "to-left-bottom", "to-left", "to-left-top"},
|
||||||
|
"",
|
||||||
|
[]string{"to top", "to right top", "to right", "to right bottom", "to bottom", "to left bottom", "to left", "to left top"},
|
||||||
|
},
|
||||||
|
RadialGradientShape: {
|
||||||
|
[]string{"ellipse", "circle"},
|
||||||
|
"",
|
||||||
|
[]string{"ellipse", "circle"},
|
||||||
|
},
|
||||||
|
RadialGradientRadius: {
|
||||||
|
[]string{"closest-side", "closest-corner", "farthest-side", "farthest-corner"},
|
||||||
|
"",
|
||||||
|
[]string{"closest-side", "closest-corner", "farthest-side", "farthest-corner"},
|
||||||
|
},
|
||||||
|
ItemCheckbox: {
|
||||||
|
[]string{"none", "single", "multiple"},
|
||||||
|
"",
|
||||||
|
[]string{"none", "single", "multiple"},
|
||||||
|
},
|
||||||
|
Float: {
|
||||||
|
[]string{"none", "left", "right"},
|
||||||
|
"float",
|
||||||
|
[]string{"none", "left", "right"},
|
||||||
|
},
|
||||||
|
Preload: {
|
||||||
|
[]string{"none", "metadata", "auto"},
|
||||||
|
"",
|
||||||
|
[]string{"none", "metadata", "auto"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func notCompatibleType(tag string, value interface{}) {
|
||||||
|
ErrorLogF(`"%T" type not compatible with "%s" property`, value, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidPropertyValue(tag string, value interface{}) {
|
||||||
|
ErrorLogF(`Invalid value "%v" of "%s" property`, value, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isConstantName(text string) bool {
|
||||||
|
len := len(text)
|
||||||
|
if len <= 1 || text[0] != '@' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len > 2 {
|
||||||
|
last := len - 1
|
||||||
|
if (text[1] == '`' && text[last] == '`') ||
|
||||||
|
(text[1] == '"' && text[last] == '"') ||
|
||||||
|
(text[1] == '\'' && text[last] == '\'') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !strings.ContainsAny(text, ",;|\"'`+(){}[]<>/\\*&%! \t\n\r")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isInt(value interface{}) (int, bool) {
|
||||||
|
var n int
|
||||||
|
switch value := value.(type) {
|
||||||
|
case int:
|
||||||
|
n = value
|
||||||
|
|
||||||
|
case int8:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
case int16:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
case int32:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
case int64:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
case uint:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
case uint8:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
case uint16:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
case uint32:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
case uint64:
|
||||||
|
n = int(value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setSimpleProperty(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
delete(properties.properties, tag)
|
||||||
|
return true
|
||||||
|
} else if text, ok := value.(string); ok {
|
||||||
|
text = strings.Trim(text, " \t\n\r")
|
||||||
|
if text == "" {
|
||||||
|
delete(properties.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if isConstantName(text) {
|
||||||
|
properties.properties[tag] = text
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setSizeProperty(tag string, value interface{}) bool {
|
||||||
|
if !properties.setSimpleProperty(tag, value) {
|
||||||
|
var size SizeUnit
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
var ok bool
|
||||||
|
if size, ok = StringToSizeUnit(value); !ok {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case SizeUnit:
|
||||||
|
size = value
|
||||||
|
|
||||||
|
case float32:
|
||||||
|
size.Type = SizeInPixel
|
||||||
|
size.Value = float64(value)
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
size.Type = SizeInPixel
|
||||||
|
size.Value = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
size.Type = SizeInPixel
|
||||||
|
size.Value = float64(n)
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if size.Type == Auto {
|
||||||
|
delete(properties.properties, tag)
|
||||||
|
} else {
|
||||||
|
properties.properties[tag] = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setAngleProperty(tag string, value interface{}) bool {
|
||||||
|
if !properties.setSimpleProperty(tag, value) {
|
||||||
|
var angle AngleUnit
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
var ok bool
|
||||||
|
if angle, ok = StringToAngleUnit(value); !ok {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case AngleUnit:
|
||||||
|
angle = value
|
||||||
|
|
||||||
|
case float32:
|
||||||
|
angle = Rad(float64(value))
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
angle = Rad(value)
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
angle = Rad(float64(n))
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties.properties[tag] = angle
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setColorProperty(tag string, value interface{}) bool {
|
||||||
|
if !properties.setSimpleProperty(tag, value) {
|
||||||
|
var result Color
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
var ok bool
|
||||||
|
if result, ok = StringToColor(value); !ok {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case Color:
|
||||||
|
result = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
if color, ok := isInt(value); ok {
|
||||||
|
result = Color(color)
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result == 0 {
|
||||||
|
delete(properties.properties, tag)
|
||||||
|
} else {
|
||||||
|
properties.properties[tag] = result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setEnumProperty(tag string, value interface{}, values []string) bool {
|
||||||
|
if !properties.setSimpleProperty(tag, value) {
|
||||||
|
var n int
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
if n, ok = enumStringToInt(text, values, false); !ok {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if i, ok := isInt(value); ok {
|
||||||
|
if i < 0 || i >= len(values) {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
n = i
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.properties[tag] = n
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setBoolProperty(tag string, value interface{}) bool {
|
||||||
|
if !properties.setSimpleProperty(tag, value) {
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
switch strings.ToLower(strings.Trim(text, " \t")) {
|
||||||
|
case "true", "yes", "on", "1":
|
||||||
|
properties.properties[tag] = true
|
||||||
|
|
||||||
|
case "false", "no", "off", "0":
|
||||||
|
properties.properties[tag] = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if n, ok := isInt(value); ok {
|
||||||
|
switch n {
|
||||||
|
case 1:
|
||||||
|
properties.properties[tag] = true
|
||||||
|
|
||||||
|
case 0:
|
||||||
|
properties.properties[tag] = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if b, ok := value.(bool); ok {
|
||||||
|
properties.properties[tag] = b
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setIntProperty(tag string, value interface{}) bool {
|
||||||
|
if !properties.setSimpleProperty(tag, value) {
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
n, err := strconv.Atoi(strings.Trim(text, " \t"))
|
||||||
|
if err != nil {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
properties.properties[tag] = n
|
||||||
|
} else if n, ok := isInt(value); ok {
|
||||||
|
properties.properties[tag] = n
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setFloatProperty(tag string, value interface{}, min, max float64) bool {
|
||||||
|
if !properties.setSimpleProperty(tag, value) {
|
||||||
|
f := float64(0)
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
var err error
|
||||||
|
if f, err = strconv.ParseFloat(strings.Trim(value, " \t"), 64); err != nil {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case float32:
|
||||||
|
f = float64(value)
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
f = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
f = float64(n)
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f >= min && f <= max {
|
||||||
|
properties.properties[tag] = f
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) Set(tag string, value interface{}) bool {
|
||||||
|
return properties.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
delete(properties.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := sizeProperties[tag]; ok {
|
||||||
|
return properties.setSizeProperty(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if valuesData, ok := enumProperties[tag]; ok {
|
||||||
|
return properties.setEnumProperty(tag, value, valuesData.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
if limits, ok := floatProperties[tag]; ok {
|
||||||
|
return properties.setFloatProperty(tag, value, limits.min, limits.max)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPropertyInList(tag, colorProperties) {
|
||||||
|
return properties.setColorProperty(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPropertyInList(tag, angleProperties) {
|
||||||
|
return properties.setAngleProperty(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPropertyInList(tag, boolProperties) {
|
||||||
|
return properties.setBoolProperty(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPropertyInList(tag, intProperties) {
|
||||||
|
return properties.setIntProperty(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
properties.properties[tag] = text
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Visible - default value of the view Visibility property: View is visible
|
||||||
|
Visible = 0
|
||||||
|
// Invisible - value of the view Visibility property: View is invisible but takes place
|
||||||
|
Invisible = 1
|
||||||
|
// Gone - value of the view Visibility property: View is invisible and does not take place
|
||||||
|
Gone = 2
|
||||||
|
|
||||||
|
// NoneTextTransform - not transform text
|
||||||
|
NoneTextTransform = 0
|
||||||
|
// CapitalizeTextTransform - capitalize text
|
||||||
|
CapitalizeTextTransform = 1
|
||||||
|
// LowerCaseTextTransform - transform text to lower case
|
||||||
|
LowerCaseTextTransform = 2
|
||||||
|
// UpperCaseTextTransform - transform text to upper case
|
||||||
|
UpperCaseTextTransform = 3
|
||||||
|
|
||||||
|
// HorizontalTopToBottom - content flows horizontally from left to right, vertically from top to bottom.
|
||||||
|
// The next horizontal line is positioned below the previous line.
|
||||||
|
HorizontalTopToBottom = 0
|
||||||
|
// HorizontalBottomToTop - content flows horizontally from left to right, vertically from bottom to top.
|
||||||
|
// The next horizontal line is positioned above the previous line.
|
||||||
|
HorizontalBottomToTop = 1
|
||||||
|
// VerticalRightToLeft - content flows vertically from top to bottom, horizontally from right to left.
|
||||||
|
// The next vertical line is positioned to the left of the previous line.
|
||||||
|
VerticalRightToLeft = 2
|
||||||
|
// VerticalLeftToRight - content flows vertically from top to bottom, horizontally from left to right.
|
||||||
|
// The next vertical line is positioned to the right of the previous line.
|
||||||
|
VerticalLeftToRight = 3
|
||||||
|
|
||||||
|
// MixedTextOrientation - rotates the characters of horizontal scripts 90° clockwise.
|
||||||
|
// Lays out the characters of vertical scripts naturally. Default value.
|
||||||
|
MixedTextOrientation = 0
|
||||||
|
// UprightTextOrientation - lays out the characters of horizontal scripts naturally (upright),
|
||||||
|
// as well as the glyphs for vertical scripts. Note that this keyword causes all characters
|
||||||
|
// to be considered as left-to-right: the used value of "text-direction" is forced to be "left-to-right".
|
||||||
|
UprightTextOrientation = 1
|
||||||
|
|
||||||
|
// SystemTextDirection - direction of a text and other elements defined by system. This is the default value.
|
||||||
|
SystemTextDirection = 0
|
||||||
|
// LeftToRightDirection - text and other elements go from left to right.
|
||||||
|
LeftToRightDirection = 1
|
||||||
|
//RightToLeftDirection - text and other elements go from right to left.
|
||||||
|
RightToLeftDirection = 2
|
||||||
|
|
||||||
|
// ThinFont - the value of "text-weight" property: the thin (hairline) text weight
|
||||||
|
ThinFont = 1
|
||||||
|
// ExtraLightFont - the value of "text-weight" property: the extra light (ultra light) text weight
|
||||||
|
ExtraLightFont = 2
|
||||||
|
// LightFont - the value of "text-weight" property: the light text weight
|
||||||
|
LightFont = 3
|
||||||
|
// NormalFont - the value of "text-weight" property (default value): the normal text weight
|
||||||
|
NormalFont = 4
|
||||||
|
// MediumFont - the value of "text-weight" property: the medium text weight
|
||||||
|
MediumFont = 5
|
||||||
|
// SemiBoldFont - the value of "text-weight" property: the semi bold (demi bold) text weight
|
||||||
|
SemiBoldFont = 6
|
||||||
|
// BoldFont - the value of "text-weight" property: the bold text weight
|
||||||
|
BoldFont = 7
|
||||||
|
// ExtraBoldFont - the value of "text-weight" property: the extra bold (ultra bold) text weight
|
||||||
|
ExtraBoldFont = 8
|
||||||
|
// BlackFont - the value of "text-weight" property: the black (heavy) text weight
|
||||||
|
BlackFont = 9
|
||||||
|
|
||||||
|
// TopAlign - top vertical-align for the "vertical-align" property
|
||||||
|
TopAlign = 0
|
||||||
|
// BottomAlign - bottom vertical-align for the "vertical-align" property
|
||||||
|
BottomAlign = 1
|
||||||
|
// LeftAlign - the left horizontal-align for the "horizontal-align" property
|
||||||
|
LeftAlign = 0
|
||||||
|
// RightAlign - the right horizontal-align for the "horizontal-align" property
|
||||||
|
RightAlign = 1
|
||||||
|
// CenterAlign - the center horizontal/vertical-align for the "horizontal-align"/"vertical-align" property
|
||||||
|
CenterAlign = 2
|
||||||
|
// StretchAlign - the stretch horizontal/vertical-align for the "horizontal-align"/"vertical-align" property
|
||||||
|
StretchAlign = 3
|
||||||
|
// JustifyAlign - the justify text align for "text-align" property
|
||||||
|
JustifyAlign = 3
|
||||||
|
// BaselineAlign - the baseline cell-vertical-align for the "cell-vertical-align" property
|
||||||
|
BaselineAlign = 4
|
||||||
|
|
||||||
|
// WhiteSpaceNormal - sequences of white space are collapsed. Newline characters in the source
|
||||||
|
// are handled the same as other white space. Lines are broken as necessary to fill line boxes.
|
||||||
|
WhiteSpaceNormal = 0
|
||||||
|
// WhiteSpaceNowrap - collapses white space as for normal, but suppresses line breaks (text wrapping)
|
||||||
|
// within the source.
|
||||||
|
WhiteSpaceNowrap = 1
|
||||||
|
// WhiteSpacePre - sequences of white space are preserved. Lines are only broken at newline
|
||||||
|
// characters in the source and at <br> elements.
|
||||||
|
WhiteSpacePre = 2
|
||||||
|
// WhiteSpacePreWrap - Sequences of white space are preserved. Lines are broken at newline
|
||||||
|
// characters, at <br>, and as necessary to fill line boxes.
|
||||||
|
WhiteSpacePreWrap = 3
|
||||||
|
// WhiteSpacePreLine - sequences of white space are collapsed. Lines are broken at newline characters,
|
||||||
|
// at <br>, and as necessary to fill line boxes.
|
||||||
|
WhiteSpacePreLine = 4
|
||||||
|
// WhiteSpaceBreakSpaces - the behavior is identical to that of WhiteSpacePreWrap, except that:
|
||||||
|
// * Any sequence of preserved white space always takes up space, including at the end of the line.
|
||||||
|
// * A line breaking opportunity exists after every preserved white space character,
|
||||||
|
// including between white space characters.
|
||||||
|
// * Such preserved spaces take up space and do not hang, and thus affect the box’s intrinsic sizes
|
||||||
|
// (min-content size and max-content size).
|
||||||
|
WhiteSpaceBreakSpaces = 5
|
||||||
|
|
||||||
|
// WordBreakNormal - use the default line break rule.
|
||||||
|
WordBreakNormal = 0
|
||||||
|
// WordBreakAll - to prevent overflow, word breaks should be inserted between any two characters
|
||||||
|
// (excluding Chinese/Japanese/Korean text).
|
||||||
|
WordBreakAll = 1
|
||||||
|
// WordBreakKeepAll - word breaks should not be used for Chinese/Japanese/Korean (CJK) text.
|
||||||
|
// Non-CJK text behavior is the same as for normal.
|
||||||
|
WordBreakKeepAll = 2
|
||||||
|
// WordBreakWord - when the block boundaries are exceeded, the remaining whole words can be split
|
||||||
|
// in an arbitrary place, unless a more suitable place for the line break is found.
|
||||||
|
WordBreakWord = 3
|
||||||
|
|
||||||
|
// TextOverflowClip - truncate the text at the limit of the content area, therefore the truncation
|
||||||
|
// can happen in the middle of a character.
|
||||||
|
TextOverflowClip = 0
|
||||||
|
// TextOverflowEllipsis - display an ellipsis ('…', U+2026 HORIZONTAL ELLIPSIS) to represent clipped text.
|
||||||
|
// The ellipsis is displayed inside the content area, decreasing the amount of text displayed.
|
||||||
|
// If there is not enough space to display the ellipsis, it is clipped.
|
||||||
|
TextOverflowEllipsis = 1
|
||||||
|
|
||||||
|
// DefaultSemantics - default value of the view Semantic property
|
||||||
|
DefaultSemantics = 0
|
||||||
|
// ArticleSemantics - value of the view Semantic property: view represents a self-contained
|
||||||
|
// composition in a document, page, application, or site, which is intended to be
|
||||||
|
// independently distributable or reusable (e.g., in syndication)
|
||||||
|
ArticleSemantics = 1
|
||||||
|
// SectionSemantics - value of the view Semantic property: view represents
|
||||||
|
// a generic standalone section of a document, which doesn't have a more specific
|
||||||
|
// semantic element to represent it.
|
||||||
|
SectionSemantics = 2
|
||||||
|
// AsideSemantics - value of the view Semantic property: view represents a portion
|
||||||
|
// of a document whose content is only indirectly related to the document's main content.
|
||||||
|
// Asides are frequently presented as sidebars or call-out boxes.
|
||||||
|
AsideSemantics = 3
|
||||||
|
// HeaderSemantics - value of the view Semantic property: view represents introductory
|
||||||
|
// content, typically a group of introductory or navigational aids. It may contain
|
||||||
|
// some heading elements but also a logo, a search form, an author name, and other elements.
|
||||||
|
HeaderSemantics = 4
|
||||||
|
// MainSemantics - value of the view Semantic property: view represents the dominant content
|
||||||
|
// of the application. The main content area consists of content that is directly related
|
||||||
|
// to or expands upon the central topic of a document, or the central functionality of an application.
|
||||||
|
MainSemantics = 5
|
||||||
|
// FooterSemantics - value of the view Semantic property: view represents a footer for its
|
||||||
|
// nearest sectioning content or sectioning root element. A footer view typically contains
|
||||||
|
// information about the author of the section, copyright data or links to related documents.
|
||||||
|
FooterSemantics = 6
|
||||||
|
// NavigationSemantics - value of the view Semantic property: view represents a section of
|
||||||
|
// a page whose purpose is to provide navigation links, either within the current document
|
||||||
|
// or to other documents. Common examples of navigation sections are menus, tables of contents,
|
||||||
|
// and indexes.
|
||||||
|
NavigationSemantics = 7
|
||||||
|
// FigureSemantics - value of the view Semantic property: view represents self-contained content,
|
||||||
|
// potentially with an optional caption, which is specified using the FigureCaptionSemantics view.
|
||||||
|
FigureSemantics = 8
|
||||||
|
// FigureCaptionSemantics - value of the view Semantic property: view represents a caption or
|
||||||
|
// legend describing the rest of the contents of its parent FigureSemantics view.
|
||||||
|
FigureCaptionSemantics = 9
|
||||||
|
// ButtonSemantics - value of the view Semantic property: view a clickable button
|
||||||
|
ButtonSemantics = 10
|
||||||
|
// ParagraphSemantics - value of the view Semantic property: view represents a paragraph.
|
||||||
|
// Paragraphs are usually represented in visual media as blocks of text separated
|
||||||
|
// from adjacent blocks by blank lines and/or first-line indentation
|
||||||
|
ParagraphSemantics = 11
|
||||||
|
// H1Semantics - value of the view Semantic property: view represent of first level section headings.
|
||||||
|
// H1Semantics is the highest section level and H6Semantics is the lowest.
|
||||||
|
H1Semantics = 12
|
||||||
|
// H2Semantics - value of the view Semantic property: view represent of second level section headings.
|
||||||
|
// H1Semantics is the highest section level and H6Semantics is the lowest.
|
||||||
|
H2Semantics = 13
|
||||||
|
// H3Semantics - value of the view Semantic property: view represent of third level section headings.
|
||||||
|
// H1Semantics is the highest section level and H6Semantics is the lowest.
|
||||||
|
H3Semantics = 14
|
||||||
|
// H4Semantics - value of the view Semantic property: view represent of fourth level section headings.
|
||||||
|
// H1Semantics is the highest section level and H6Semantics is the lowest.
|
||||||
|
H4Semantics = 15
|
||||||
|
// H5Semantics - value of the view Semantic property: view represent of fifth level section headings.
|
||||||
|
// H1Semantics is the highest section level and H6Semantics is the lowest.
|
||||||
|
H5Semantics = 16
|
||||||
|
// H6Semantics - value of the view Semantic property: view represent of sixth level section headings.
|
||||||
|
// H1Semantics is the highest section level and H6Semantics is the lowest.
|
||||||
|
H6Semantics = 17
|
||||||
|
// BlockquoteSemantics - value of the view Semantic property: view indicates that
|
||||||
|
// the enclosed text is an extended quotation.
|
||||||
|
BlockquoteSemantics = 18
|
||||||
|
// CodeSemantics - value of the view Semantic property: view displays its contents styled
|
||||||
|
// in a fashion intended to indicate that the text is a short fragment of computer code
|
||||||
|
CodeSemantics = 19
|
||||||
|
|
||||||
|
// NoneFloat - value of the view "float" property: the View must not float.
|
||||||
|
NoneFloat = 0
|
||||||
|
// LeftFloat - value of the view "float" property: the View must float on the left side of its containing block.
|
||||||
|
LeftFloat = 1
|
||||||
|
// RightFloat - value of the view "float" property: the View must float on the right side of its containing block.
|
||||||
|
RightFloat = 2
|
||||||
|
)
|
|
@ -0,0 +1,770 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Radius is the SizeUnit view property that determines the corners rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
Radius = "radius"
|
||||||
|
// RadiusX is the SizeUnit view property that determines the x-axis corners elliptic rounding
|
||||||
|
// radius of an element's outer border edge.
|
||||||
|
RadiusX = "radius-x"
|
||||||
|
// RadiusY is the SizeUnit view property that determines the y-axis corners elliptic rounding
|
||||||
|
// radius of an element's outer border edge.
|
||||||
|
RadiusY = "radius-y"
|
||||||
|
// RadiusTopLeft is the SizeUnit view property that determines the top-left corner rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
RadiusTopLeft = "radius-top-left"
|
||||||
|
// RadiusTopLeftX is the SizeUnit view property that determines the x-axis top-left corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
RadiusTopLeftX = "radius-top-left-x"
|
||||||
|
// RadiusTopLeftY is the SizeUnit view property that determines the y-axis top-left corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
RadiusTopLeftY = "radius-top-left-y"
|
||||||
|
// RadiusTopRight is the SizeUnit view property that determines the top-right corner rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
RadiusTopRight = "radius-top-right"
|
||||||
|
// RadiusTopRightX is the SizeUnit view property that determines the x-axis top-right corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
RadiusTopRightX = "radius-top-right-x"
|
||||||
|
// RadiusTopRightY is the SizeUnit view property that determines the y-axis top-right corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
RadiusTopRightY = "radius-top-right-y"
|
||||||
|
// RadiusBottomLeft is the SizeUnit view property that determines the bottom-left corner rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
RadiusBottomLeft = "radius-bottom-left"
|
||||||
|
// RadiusBottomLeftX is the SizeUnit view property that determines the x-axis bottom-left corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
RadiusBottomLeftX = "radius-bottom-left-x"
|
||||||
|
// RadiusBottomLeftY is the SizeUnit view property that determines the y-axis bottom-left corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
RadiusBottomLeftY = "radius-bottom-left-y"
|
||||||
|
// RadiusBottomRight is the SizeUnit view property that determines the bottom-right corner rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
RadiusBottomRight = "radius-bottom-right"
|
||||||
|
// RadiusBottomRightX is the SizeUnit view property that determines the x-axis bottom-right corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
RadiusBottomRightX = "radius-bottom-right-x"
|
||||||
|
// RadiusBottomRightY is the SizeUnit view property that determines the y-axis bottom-right corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
RadiusBottomRightY = "radius-bottom-right-y"
|
||||||
|
// X is the SizeUnit property of the ShadowProperty that determines the x-axis corners elliptic rounding
|
||||||
|
// radius of an element's outer border edge.
|
||||||
|
X = "x"
|
||||||
|
// Y is the SizeUnit property of the ShadowProperty that determines the y-axis corners elliptic rounding
|
||||||
|
// radius of an element's outer border edge.
|
||||||
|
Y = "y"
|
||||||
|
// TopLeft is the SizeUnit property of the ShadowProperty that determines the top-left corner rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
TopLeft = "top-left"
|
||||||
|
// TopLeftX is the SizeUnit property of the ShadowProperty that determines the x-axis top-left corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
TopLeftX = "top-left-x"
|
||||||
|
// TopLeftY is the SizeUnit property of the ShadowProperty that determines the y-axis top-left corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
TopLeftY = "top-left-y"
|
||||||
|
// TopRight is the SizeUnit property of the ShadowProperty that determines the top-right corner rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
TopRight = "top-right"
|
||||||
|
// TopRightX is the SizeUnit property of the ShadowProperty that determines the x-axis top-right corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
TopRightX = "top-right-x"
|
||||||
|
// TopRightY is the SizeUnit property of the ShadowProperty that determines the y-axis top-right corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
TopRightY = "top-right-y"
|
||||||
|
// BottomLeft is the SizeUnit property of the ShadowProperty that determines the bottom-left corner rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
BottomLeft = "bottom-left"
|
||||||
|
// BottomLeftX is the SizeUnit property of the ShadowProperty that determines the x-axis bottom-left corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
BottomLeftX = "bottom-left-x"
|
||||||
|
// BottomLeftY is the SizeUnit property of the ShadowProperty that determines the y-axis bottom-left corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
BottomLeftY = "bottom-left-y"
|
||||||
|
// BottomRight is the SizeUnit property of the ShadowProperty that determines the bottom-right corner rounding radius
|
||||||
|
// of an element's outer border edge.
|
||||||
|
BottomRight = "bottom-right"
|
||||||
|
// BottomRightX is the SizeUnit property of the ShadowProperty that determines the x-axis bottom-right corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
BottomRightX = "bottom-right-x"
|
||||||
|
// BottomRightY is the SizeUnit property of the ShadowProperty that determines the y-axis bottom-right corner elliptic
|
||||||
|
// rounding radius of an element's outer border edge.
|
||||||
|
BottomRightY = "bottom-right-y"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RadiusProperty interface {
|
||||||
|
Properties
|
||||||
|
ruiStringer
|
||||||
|
fmt.Stringer
|
||||||
|
BoxRadius(session Session) BoxRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
type radiusPropertyData struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRadiusProperty creates the new RadiusProperty
|
||||||
|
func NewRadiusProperty(params Params) RadiusProperty {
|
||||||
|
result := new(radiusPropertyData)
|
||||||
|
result.properties = map[string]interface{}{}
|
||||||
|
if params != nil {
|
||||||
|
for _, tag := range []string{X, Y, TopLeft, TopRight, BottomLeft, BottomRight, TopLeftX, TopLeftY,
|
||||||
|
TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY} {
|
||||||
|
if value, ok := params[tag]; ok {
|
||||||
|
result.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) normalizeTag(tag string) string {
|
||||||
|
return strings.TrimPrefix(strings.ToLower(tag), "radius-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) ruiString(writer ruiWriter) {
|
||||||
|
writer.startObject("_")
|
||||||
|
|
||||||
|
for _, tag := range []string{X, Y, TopLeft, TopLeftX, TopLeftY, TopRight, TopRightX, TopRightY,
|
||||||
|
BottomLeft, BottomLeftX, BottomLeftY, BottomRight, BottomRightX, BottomRightY} {
|
||||||
|
if value, ok := radius.properties[tag]; ok {
|
||||||
|
writer.writeProperty(Style, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
radius.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) delete(tags []string) {
|
||||||
|
for _, tag := range tags {
|
||||||
|
delete(radius.properties, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) deleteUnusedTags() {
|
||||||
|
for _, tag := range []string{X, Y} {
|
||||||
|
if _, ok := radius.properties[tag]; ok {
|
||||||
|
unused := true
|
||||||
|
for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} {
|
||||||
|
if _, ok := radius.properties[t+"-"+tag]; !ok {
|
||||||
|
if _, ok := radius.properties[t]; !ok {
|
||||||
|
unused = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if unused {
|
||||||
|
delete(radius.properties, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
equalValue := func(value1, value2 interface{}) bool {
|
||||||
|
switch value1 := value1.(type) {
|
||||||
|
case string:
|
||||||
|
switch value2 := value2.(type) {
|
||||||
|
case string:
|
||||||
|
return value1 == value2
|
||||||
|
}
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
switch value2 := value2.(type) {
|
||||||
|
case SizeUnit:
|
||||||
|
return value1.Equal(value2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range []string{TopLeft, TopRight, BottomLeft, BottomRight} {
|
||||||
|
tagX := tag + "-x"
|
||||||
|
tagY := tag + "-y"
|
||||||
|
valueX, okX := radius.properties[tagX]
|
||||||
|
valueY, okY := radius.properties[tagY]
|
||||||
|
|
||||||
|
if value, ok := radius.properties[tag]; ok {
|
||||||
|
if okX && okY {
|
||||||
|
delete(radius.properties, tag)
|
||||||
|
} else if okX && !okY {
|
||||||
|
if equalValue(value, valueX) {
|
||||||
|
delete(radius.properties, tagX)
|
||||||
|
} else {
|
||||||
|
radius.properties[tagY] = value
|
||||||
|
delete(radius.properties, tag)
|
||||||
|
}
|
||||||
|
} else if !okX && okY {
|
||||||
|
if equalValue(value, valueY) {
|
||||||
|
delete(radius.properties, tagY)
|
||||||
|
} else {
|
||||||
|
radius.properties[tagX] = value
|
||||||
|
delete(radius.properties, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if okX && okY && equalValue(valueX, valueY) {
|
||||||
|
radius.properties[tag] = valueX
|
||||||
|
delete(radius.properties, tagX)
|
||||||
|
delete(radius.properties, tagY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) Remove(tag string) {
|
||||||
|
tag = radius.normalizeTag(tag)
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case X, Y:
|
||||||
|
if _, ok := radius.properties[tag]; ok {
|
||||||
|
radius.Set(tag, AutoSize())
|
||||||
|
delete(radius.properties, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY:
|
||||||
|
delete(radius.properties, tag)
|
||||||
|
|
||||||
|
case TopLeft, TopRight, BottomLeft, BottomRight:
|
||||||
|
radius.delete([]string{tag, tag + "-x", tag + "-y"})
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF(`"%s" property is not compatible with the RadiusProperty`, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) Set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
radius.Remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = radius.normalizeTag(tag)
|
||||||
|
switch tag {
|
||||||
|
case X:
|
||||||
|
if radius.setSizeProperty(tag, value) {
|
||||||
|
radius.delete([]string{TopLeftX, TopRightX, BottomLeftX, BottomRightX})
|
||||||
|
for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} {
|
||||||
|
if val, ok := radius.properties[t]; ok {
|
||||||
|
if _, ok := radius.properties[t+"-y"]; !ok {
|
||||||
|
radius.properties[t+"-y"] = val
|
||||||
|
}
|
||||||
|
delete(radius.properties, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case Y:
|
||||||
|
if radius.setSizeProperty(tag, value) {
|
||||||
|
radius.delete([]string{TopLeftY, TopRightY, BottomLeftY, BottomRightY})
|
||||||
|
for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} {
|
||||||
|
if val, ok := radius.properties[t]; ok {
|
||||||
|
if _, ok := radius.properties[t+"-x"]; !ok {
|
||||||
|
radius.properties[t+"-x"] = val
|
||||||
|
}
|
||||||
|
delete(radius.properties, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY:
|
||||||
|
if radius.setSizeProperty(tag, value) {
|
||||||
|
radius.deleteUnusedTags()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case TopLeft, TopRight, BottomLeft, BottomRight:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case SizeUnit:
|
||||||
|
radius.properties[tag] = value
|
||||||
|
radius.delete([]string{tag + "-x", tag + "-y"})
|
||||||
|
radius.deleteUnusedTags()
|
||||||
|
return true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if strings.Contains(value, "/") {
|
||||||
|
if values := strings.Split(value, "/"); len(values) == 2 {
|
||||||
|
xOK := radius.Set(tag+"-x", value[0])
|
||||||
|
yOK := radius.Set(tag+"-y", value[1])
|
||||||
|
return xOK && yOK
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if radius.setSizeProperty(tag, value) {
|
||||||
|
radius.delete([]string{tag + "-x", tag + "-y"})
|
||||||
|
radius.deleteUnusedTags()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLogF(`"%s" property is not compatible with the RadiusProperty`, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) Get(tag string) interface{} {
|
||||||
|
tag = radius.normalizeTag(tag)
|
||||||
|
if value, ok := radius.properties[tag]; ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY:
|
||||||
|
tagLen := len(tag)
|
||||||
|
if value, ok := radius.properties[tag[:tagLen-2]]; ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
if value, ok := radius.properties[tag[tagLen-1:]]; ok {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius *radiusPropertyData) BoxRadius(session Session) BoxRadius {
|
||||||
|
x, _ := sizeProperty(radius, X, session)
|
||||||
|
y, _ := sizeProperty(radius, Y, session)
|
||||||
|
|
||||||
|
getRadius := func(tag string) (SizeUnit, SizeUnit) {
|
||||||
|
rx := x
|
||||||
|
ry := y
|
||||||
|
if r, ok := sizeProperty(radius, tag, session); ok {
|
||||||
|
rx = r
|
||||||
|
ry = r
|
||||||
|
}
|
||||||
|
if r, ok := sizeProperty(radius, tag+"-x", session); ok {
|
||||||
|
rx = r
|
||||||
|
}
|
||||||
|
if r, ok := sizeProperty(radius, tag+"-y", session); ok {
|
||||||
|
ry = r
|
||||||
|
}
|
||||||
|
|
||||||
|
return rx, ry
|
||||||
|
}
|
||||||
|
|
||||||
|
var result BoxRadius
|
||||||
|
|
||||||
|
result.TopLeftX, result.TopLeftY = getRadius(TopLeft)
|
||||||
|
result.TopRightX, result.TopRightY = getRadius(TopRight)
|
||||||
|
result.BottomLeftX, result.BottomLeftY = getRadius(BottomLeft)
|
||||||
|
result.BottomRightX, result.BottomRightY = getRadius(BottomRight)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BoxRadius defines radii of rounds the corners of an element's outer border edge
|
||||||
|
type BoxRadius struct {
|
||||||
|
TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY SizeUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllAnglesIsEqual returns 'true' if all angles is equal, 'false' otherwise
|
||||||
|
func (radius BoxRadius) AllAnglesIsEqual() bool {
|
||||||
|
return radius.TopLeftX.Equal(radius.TopRightX) &&
|
||||||
|
radius.TopLeftY.Equal(radius.TopRightY) &&
|
||||||
|
radius.TopLeftX.Equal(radius.BottomLeftX) &&
|
||||||
|
radius.TopLeftY.Equal(radius.BottomLeftY) &&
|
||||||
|
radius.TopLeftX.Equal(radius.BottomRightX) &&
|
||||||
|
radius.TopLeftY.Equal(radius.BottomRightY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of a BoxRadius struct
|
||||||
|
func (radius BoxRadius) String() string {
|
||||||
|
|
||||||
|
if radius.AllAnglesIsEqual() {
|
||||||
|
if radius.TopLeftX.Equal(radius.TopLeftY) {
|
||||||
|
return radius.TopLeftX.String()
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("_{ x = %s, y = %s }", radius.TopLeftX.String(), radius.TopLeftY.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString("_{ ")
|
||||||
|
|
||||||
|
if radius.TopLeftX.Equal(radius.TopLeftY) {
|
||||||
|
buffer.WriteString("top-left = ")
|
||||||
|
buffer.WriteString(radius.TopLeftX.String())
|
||||||
|
} else {
|
||||||
|
buffer.WriteString("top-left-x = ")
|
||||||
|
buffer.WriteString(radius.TopLeftX.String())
|
||||||
|
buffer.WriteString("top-left-y = ")
|
||||||
|
buffer.WriteString(radius.TopLeftY.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if radius.TopRightX.Equal(radius.TopRightY) {
|
||||||
|
buffer.WriteString(", top-right = ")
|
||||||
|
buffer.WriteString(radius.TopRightX.String())
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(", top-right-x = ")
|
||||||
|
buffer.WriteString(radius.TopRightX.String())
|
||||||
|
buffer.WriteString(", top-right-y = ")
|
||||||
|
buffer.WriteString(radius.TopRightY.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if radius.BottomLeftX.Equal(radius.BottomLeftY) {
|
||||||
|
buffer.WriteString(", bottom-left = ")
|
||||||
|
buffer.WriteString(radius.BottomLeftX.String())
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(", bottom-left-x = ")
|
||||||
|
buffer.WriteString(radius.BottomLeftX.String())
|
||||||
|
buffer.WriteString(", bottom-left-y = ")
|
||||||
|
buffer.WriteString(radius.BottomLeftY.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if radius.BottomRightX.Equal(radius.BottomRightY) {
|
||||||
|
buffer.WriteString(", bottom-right = ")
|
||||||
|
buffer.WriteString(radius.BottomRightX.String())
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(", bottom-right-x = ")
|
||||||
|
buffer.WriteString(radius.BottomRightX.String())
|
||||||
|
buffer.WriteString(", bottom-right-y = ")
|
||||||
|
buffer.WriteString(radius.BottomRightY.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(" }")
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius BoxRadius) cssValue(builder cssBuilder) {
|
||||||
|
|
||||||
|
if (radius.TopLeftX.Type == Auto || radius.TopLeftX.Value == 0) &&
|
||||||
|
(radius.TopLeftY.Type == Auto || radius.TopLeftY.Value == 0) &&
|
||||||
|
(radius.TopRightX.Type == Auto || radius.TopRightX.Value == 0) &&
|
||||||
|
(radius.TopRightY.Type == Auto || radius.TopRightY.Value == 0) &&
|
||||||
|
(radius.BottomRightX.Type == Auto || radius.BottomRightX.Value == 0) &&
|
||||||
|
(radius.BottomRightY.Type == Auto || radius.BottomRightY.Value == 0) &&
|
||||||
|
(radius.BottomLeftX.Type == Auto || radius.BottomLeftX.Value == 0) &&
|
||||||
|
(radius.BottomLeftY.Type == Auto || radius.BottomLeftY.Value == 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(radius.TopLeftX.cssString("0"))
|
||||||
|
|
||||||
|
if radius.AllAnglesIsEqual() {
|
||||||
|
|
||||||
|
if !radius.TopLeftX.Equal(radius.TopLeftY) {
|
||||||
|
buffer.WriteString(" / ")
|
||||||
|
buffer.WriteString(radius.TopLeftY.cssString("0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(radius.TopRightX.cssString("0"))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(radius.BottomRightX.cssString("0"))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(radius.BottomLeftX.cssString("0"))
|
||||||
|
|
||||||
|
if !radius.TopLeftX.Equal(radius.TopLeftY) ||
|
||||||
|
!radius.TopRightX.Equal(radius.TopRightY) ||
|
||||||
|
!radius.BottomLeftX.Equal(radius.BottomLeftY) ||
|
||||||
|
!radius.BottomRightX.Equal(radius.BottomRightY) {
|
||||||
|
|
||||||
|
buffer.WriteString(" / ")
|
||||||
|
buffer.WriteString(radius.TopLeftY.cssString("0"))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(radius.TopRightY.cssString("0"))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(radius.BottomRightY.cssString("0"))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(radius.BottomLeftY.cssString("0"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add("border-radius", buffer.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (radius BoxRadius) cssString() string {
|
||||||
|
var builder cssValueBuilder
|
||||||
|
radius.cssValue(&builder)
|
||||||
|
return builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRadiusProperty(style Properties) RadiusProperty {
|
||||||
|
if value := style.Get(Radius); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case RadiusProperty:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case BoxRadius:
|
||||||
|
result := NewRadiusProperty(nil)
|
||||||
|
if value.AllAnglesIsEqual() {
|
||||||
|
result.Set(X, value.TopLeftX)
|
||||||
|
result.Set(Y, value.TopLeftY)
|
||||||
|
} else {
|
||||||
|
if value.TopLeftX.Equal(value.TopLeftY) {
|
||||||
|
result.Set(TopLeft, value.TopLeftX)
|
||||||
|
} else {
|
||||||
|
result.Set(TopLeftX, value.TopLeftX)
|
||||||
|
result.Set(TopLeftY, value.TopLeftY)
|
||||||
|
}
|
||||||
|
if value.TopRightX.Equal(value.TopRightY) {
|
||||||
|
result.Set(TopRight, value.TopRightX)
|
||||||
|
} else {
|
||||||
|
result.Set(TopRightX, value.TopRightX)
|
||||||
|
result.Set(TopRightY, value.TopRightY)
|
||||||
|
}
|
||||||
|
if value.BottomLeftX.Equal(value.BottomLeftY) {
|
||||||
|
result.Set(BottomLeft, value.BottomLeftX)
|
||||||
|
} else {
|
||||||
|
result.Set(BottomLeftX, value.BottomLeftX)
|
||||||
|
result.Set(BottomLeftY, value.BottomLeftY)
|
||||||
|
}
|
||||||
|
if value.BottomRightX.Equal(value.BottomRightY) {
|
||||||
|
result.Set(BottomRight, value.BottomRightX)
|
||||||
|
} else {
|
||||||
|
result.Set(BottomRightX, value.BottomRightX)
|
||||||
|
result.Set(BottomRightY, value.BottomRightY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
return NewRadiusProperty(Params{
|
||||||
|
X: value,
|
||||||
|
Y: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
case string:
|
||||||
|
return NewRadiusProperty(Params{
|
||||||
|
X: value,
|
||||||
|
Y: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewRadiusProperty(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setRadius(value interface{}) bool {
|
||||||
|
|
||||||
|
if value == nil {
|
||||||
|
delete(properties.properties, Radius)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case RadiusProperty:
|
||||||
|
properties.properties[Radius] = value
|
||||||
|
return true
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
properties.properties[Radius] = value
|
||||||
|
return true
|
||||||
|
|
||||||
|
case BoxRadius:
|
||||||
|
radius := NewRadiusProperty(nil)
|
||||||
|
if value.AllAnglesIsEqual() {
|
||||||
|
radius.Set(X, value.TopLeftX)
|
||||||
|
radius.Set(Y, value.TopLeftY)
|
||||||
|
} else {
|
||||||
|
if value.TopLeftX.Equal(value.TopLeftY) {
|
||||||
|
radius.Set(TopLeft, value.TopLeftX)
|
||||||
|
} else {
|
||||||
|
radius.Set(TopLeftX, value.TopLeftX)
|
||||||
|
radius.Set(TopLeftY, value.TopLeftY)
|
||||||
|
}
|
||||||
|
if value.TopRightX.Equal(value.TopRightY) {
|
||||||
|
radius.Set(TopRight, value.TopRightX)
|
||||||
|
} else {
|
||||||
|
radius.Set(TopRightX, value.TopRightX)
|
||||||
|
radius.Set(TopRightY, value.TopRightY)
|
||||||
|
}
|
||||||
|
if value.BottomLeftX.Equal(value.BottomLeftY) {
|
||||||
|
radius.Set(BottomLeft, value.BottomLeftX)
|
||||||
|
} else {
|
||||||
|
radius.Set(BottomLeftX, value.BottomLeftX)
|
||||||
|
radius.Set(BottomLeftY, value.BottomLeftY)
|
||||||
|
}
|
||||||
|
if value.BottomRightX.Equal(value.BottomRightY) {
|
||||||
|
radius.Set(BottomRight, value.BottomRightX)
|
||||||
|
} else {
|
||||||
|
radius.Set(BottomRightX, value.BottomRightX)
|
||||||
|
radius.Set(BottomRightY, value.BottomRightY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties.properties[Radius] = radius
|
||||||
|
return true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if strings.Contains(value, "/") {
|
||||||
|
values := strings.Split(value, "/")
|
||||||
|
if len(values) == 2 {
|
||||||
|
okX := properties.setRadiusElement(RadiusX, values[0])
|
||||||
|
okY := properties.setRadiusElement(RadiusY, values[1])
|
||||||
|
return okX && okY
|
||||||
|
} else {
|
||||||
|
notCompatibleType(Radius, value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return properties.setSizeProperty(Radius, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
radius := NewRadiusProperty(nil)
|
||||||
|
for _, tag := range []string{X, Y, TopLeft, TopRight, BottomLeft, BottomRight, TopLeftX, TopLeftY,
|
||||||
|
TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY} {
|
||||||
|
if value, ok := value.PropertyValue(tag); ok {
|
||||||
|
radius.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties.properties[Radius] = radius
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(Radius, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) removeRadiusElement(tag string) {
|
||||||
|
if value, ok := properties.properties[Radius]; ok && value != nil {
|
||||||
|
radius := getRadiusProperty(properties)
|
||||||
|
radius.Remove(tag)
|
||||||
|
if len(radius.AllTags()) == 0 {
|
||||||
|
delete(properties.properties, Radius)
|
||||||
|
} else {
|
||||||
|
properties.properties[Radius] = radius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setRadiusElement(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
properties.removeRadiusElement(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
radius := getRadiusProperty(properties)
|
||||||
|
if radius.Set(tag, value) {
|
||||||
|
properties.properties[Radius] = radius
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRadiusElement(style Properties, tag string) interface{} {
|
||||||
|
value := style.Get(Radius)
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case RadiusProperty:
|
||||||
|
return value.Get(tag)
|
||||||
|
|
||||||
|
case BoxRadius:
|
||||||
|
switch tag {
|
||||||
|
case RadiusX:
|
||||||
|
if value.TopLeftX.Equal(value.TopRightX) &&
|
||||||
|
value.TopLeftX.Equal(value.BottomLeftX) &&
|
||||||
|
value.TopLeftX.Equal(value.BottomRightX) {
|
||||||
|
return value.TopLeftX
|
||||||
|
}
|
||||||
|
|
||||||
|
case RadiusY:
|
||||||
|
if value.TopLeftY.Equal(value.TopRightY) &&
|
||||||
|
value.TopLeftY.Equal(value.BottomLeftY) &&
|
||||||
|
value.TopLeftY.Equal(value.BottomRightY) {
|
||||||
|
return value.TopLeftY
|
||||||
|
}
|
||||||
|
|
||||||
|
case RadiusTopLeft:
|
||||||
|
if value.TopLeftX.Equal(value.TopLeftY) {
|
||||||
|
return value.TopLeftY
|
||||||
|
}
|
||||||
|
|
||||||
|
case RadiusTopRight:
|
||||||
|
if value.TopRightX.Equal(value.TopRightY) {
|
||||||
|
return value.TopRightY
|
||||||
|
}
|
||||||
|
|
||||||
|
case RadiusBottomLeft:
|
||||||
|
if value.BottomLeftX.Equal(value.BottomLeftY) {
|
||||||
|
return value.BottomLeftY
|
||||||
|
}
|
||||||
|
|
||||||
|
case RadiusBottomRight:
|
||||||
|
if value.BottomRightX.Equal(value.BottomRightY) {
|
||||||
|
return value.BottomRightY
|
||||||
|
}
|
||||||
|
|
||||||
|
case RadiusTopLeftX:
|
||||||
|
return value.TopLeftX
|
||||||
|
|
||||||
|
case RadiusTopLeftY:
|
||||||
|
return value.TopLeftY
|
||||||
|
|
||||||
|
case RadiusTopRightX:
|
||||||
|
return value.TopRightX
|
||||||
|
|
||||||
|
case RadiusTopRightY:
|
||||||
|
return value.TopRightY
|
||||||
|
|
||||||
|
case RadiusBottomLeftX:
|
||||||
|
return value.BottomLeftX
|
||||||
|
|
||||||
|
case RadiusBottomLeftY:
|
||||||
|
return value.BottomLeftY
|
||||||
|
|
||||||
|
case RadiusBottomRightX:
|
||||||
|
return value.BottomRightX
|
||||||
|
|
||||||
|
case RadiusBottomRightY:
|
||||||
|
return value.BottomRightY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRadius(properties Properties, session Session) BoxRadius {
|
||||||
|
if value := properties.Get(Radius); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case BoxRadius:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case RadiusProperty:
|
||||||
|
return value.BoxRadius(session)
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
return BoxRadius{TopLeftX: value, TopLeftY: value, TopRightX: value, TopRightY: value,
|
||||||
|
BottomLeftX: value, BottomLeftY: value, BottomRightX: value, BottomRightY: value}
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
if size, ok := StringToSizeUnit(text); ok {
|
||||||
|
return BoxRadius{TopLeftX: size, TopLeftY: size, TopRightX: size, TopRightY: size,
|
||||||
|
BottomLeftX: size, BottomLeftY: size, BottomRightX: size, BottomRightY: size}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BoxRadius{}
|
||||||
|
}
|
|
@ -0,0 +1,449 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Side is the constant for the "side" property tag.
|
||||||
|
// The "side" int property determines which side of the container is used to resize.
|
||||||
|
// The value of property is or-combination of TopSide (1), RightSide (2), BottomSide (4), and LeftSide (8)
|
||||||
|
Side = "side"
|
||||||
|
// ResizeBorderWidth is the constant for the "resize-border-width" property tag.
|
||||||
|
// The "ResizeBorderWidth" SizeUnit property determines the width of the resizing border
|
||||||
|
ResizeBorderWidth = "resize-border-width"
|
||||||
|
// CellVerticalAlign is the constant for the "cell-vertical-align" property tag.
|
||||||
|
CellVerticalAlign = "cell-vertical-align"
|
||||||
|
// CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag.
|
||||||
|
CellHorizontalAlign = "cell-horizontal-align"
|
||||||
|
|
||||||
|
// TopSide is value of the "side" property: the top side is used to resize
|
||||||
|
TopSide = 1
|
||||||
|
// RightSide is value of the "side" property: the right side is used to resize
|
||||||
|
RightSide = 2
|
||||||
|
// BottomSide is value of the "side" property: the bottom side is used to resize
|
||||||
|
BottomSide = 4
|
||||||
|
// LeftSide is value of the "side" property: the left side is used to resize
|
||||||
|
LeftSide = 8
|
||||||
|
// AllSides is value of the "side" property: all sides is used to resize
|
||||||
|
AllSides = TopSide | RightSide | BottomSide | LeftSide
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resizable - grid-container of View
|
||||||
|
type Resizable interface {
|
||||||
|
View
|
||||||
|
ParanetView
|
||||||
|
}
|
||||||
|
|
||||||
|
type resizableData struct {
|
||||||
|
viewData
|
||||||
|
content []View
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResizable create new Resizable object and return it
|
||||||
|
func NewResizable(session Session, params Params) Resizable {
|
||||||
|
view := new(resizableData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newResizable(session Session) View {
|
||||||
|
return NewResizable(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) Init(session Session) {
|
||||||
|
resizable.viewData.Init(session)
|
||||||
|
resizable.tag = "Resizable"
|
||||||
|
resizable.systemClass = "ruiGridLayout"
|
||||||
|
resizable.content = []View{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) Views() []View {
|
||||||
|
return resizable.content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) Remove(tag string) {
|
||||||
|
resizable.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case Side:
|
||||||
|
oldSide := resizable.getSide()
|
||||||
|
delete(resizable.properties, Side)
|
||||||
|
if oldSide != resizable.getSide() {
|
||||||
|
updateInnerHTML(resizable.htmlID(), resizable.Session())
|
||||||
|
resizable.updateResizeBorderWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
case ResizeBorderWidth:
|
||||||
|
w := resizable.resizeBorderWidth()
|
||||||
|
delete(resizable.properties, ResizeBorderWidth)
|
||||||
|
if !w.Equal(resizable.resizeBorderWidth()) {
|
||||||
|
resizable.updateResizeBorderWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
case Content:
|
||||||
|
if len(resizable.content) > 0 {
|
||||||
|
resizable.content = []View{}
|
||||||
|
updateInnerHTML(resizable.htmlID(), resizable.Session())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
resizable.viewData.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) Set(tag string, value interface{}) bool {
|
||||||
|
return resizable.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
resizable.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Side:
|
||||||
|
oldSide := resizable.getSide()
|
||||||
|
ok := resizable.setSide(value)
|
||||||
|
if ok && oldSide != resizable.getSide() {
|
||||||
|
updateInnerHTML(resizable.htmlID(), resizable.Session())
|
||||||
|
resizable.updateResizeBorderWidth()
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
|
||||||
|
case ResizeBorderWidth:
|
||||||
|
w := resizable.resizeBorderWidth()
|
||||||
|
ok := resizable.setSizeProperty(tag, value)
|
||||||
|
if ok && !w.Equal(resizable.resizeBorderWidth()) {
|
||||||
|
resizable.updateResizeBorderWidth()
|
||||||
|
}
|
||||||
|
return ok
|
||||||
|
|
||||||
|
case Content:
|
||||||
|
var newContent View = nil
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
newContent = NewTextView(resizable.Session(), Params{Text: value})
|
||||||
|
|
||||||
|
case View:
|
||||||
|
newContent = value
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
if view := CreateViewFromObject(resizable.Session(), value); view != nil {
|
||||||
|
newContent = view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if newContent != nil {
|
||||||
|
if len(resizable.content) == 0 {
|
||||||
|
resizable.content = []View{newContent}
|
||||||
|
} else {
|
||||||
|
resizable.content[0] = newContent
|
||||||
|
}
|
||||||
|
updateInnerHTML(resizable.htmlID(), resizable.Session())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case CellWidth, CellHeight, GridRowGap, GridColumnGap, CellVerticalAlign, CellHorizontalAlign:
|
||||||
|
ErrorLogF(`Not supported "%s" property`, tag)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return resizable.viewData.set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) Get(tag string) interface{} {
|
||||||
|
return resizable.get(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) getSide() int {
|
||||||
|
if value := resizable.getRaw(Side); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if value, ok := resizable.session.resolveConstants(value); ok {
|
||||||
|
validValues := map[string]int{
|
||||||
|
"top": TopSide,
|
||||||
|
"right": RightSide,
|
||||||
|
"bottom": BottomSide,
|
||||||
|
"left": LeftSide,
|
||||||
|
"all": AllSides,
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(value, "|") {
|
||||||
|
values := strings.Split(value, "|")
|
||||||
|
sides := 0
|
||||||
|
for _, val := range values {
|
||||||
|
if n, err := strconv.Atoi(val); err == nil {
|
||||||
|
if n < 1 || n > AllSides {
|
||||||
|
return AllSides
|
||||||
|
}
|
||||||
|
sides |= n
|
||||||
|
} else if n, ok := validValues[val]; ok {
|
||||||
|
sides |= n
|
||||||
|
} else {
|
||||||
|
return AllSides
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sides
|
||||||
|
|
||||||
|
} else if n, err := strconv.Atoi(value); err == nil {
|
||||||
|
if n >= 1 || n <= AllSides {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
} else if n, ok := validValues[value]; ok {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case int:
|
||||||
|
if value >= 1 && value <= AllSides {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AllSides
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) setSide(value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if n, err := strconv.Atoi(value); err == nil {
|
||||||
|
if n >= 1 && n <= AllSides {
|
||||||
|
resizable.properties[Side] = n
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
validValues := map[string]int{
|
||||||
|
"top": TopSide,
|
||||||
|
"right": RightSide,
|
||||||
|
"bottom": BottomSide,
|
||||||
|
"left": LeftSide,
|
||||||
|
"all": AllSides,
|
||||||
|
}
|
||||||
|
if strings.Contains(value, "|") {
|
||||||
|
values := strings.Split(value, "|")
|
||||||
|
sides := 0
|
||||||
|
hasConst := false
|
||||||
|
for i, val := range values {
|
||||||
|
val := strings.Trim(val, " \t\r\n")
|
||||||
|
values[i] = val
|
||||||
|
|
||||||
|
if val[0] == '@' {
|
||||||
|
hasConst = true
|
||||||
|
} else if n, err := strconv.Atoi(val); err == nil {
|
||||||
|
if n < 1 || n > AllSides {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sides |= n
|
||||||
|
} else if n, ok := validValues[val]; ok {
|
||||||
|
sides |= n
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasConst {
|
||||||
|
value = values[0]
|
||||||
|
for i := 1; i < len(values); i++ {
|
||||||
|
value += "|" + values[i]
|
||||||
|
}
|
||||||
|
resizable.properties[Side] = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if sides >= 1 && sides <= AllSides {
|
||||||
|
resizable.properties[Side] = sides
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if value[0] == '@' {
|
||||||
|
resizable.properties[Side] = value
|
||||||
|
return true
|
||||||
|
} else if n, ok := validValues[value]; ok {
|
||||||
|
resizable.properties[Side] = n
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case int:
|
||||||
|
if value >= 1 && value <= AllSides {
|
||||||
|
resizable.properties[Side] = value
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`Invalid value %d of "side" property`, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
if n >= 1 && n <= AllSides {
|
||||||
|
resizable.properties[Side] = n
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`Invalid value %d of "side" property`, n)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) resizeBorderWidth() SizeUnit {
|
||||||
|
result, _ := sizeProperty(resizable, ResizeBorderWidth, resizable.Session())
|
||||||
|
if result.Type == Auto || result.Value == 0 {
|
||||||
|
return Px(4)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) updateResizeBorderWidth() {
|
||||||
|
htmlID := resizable.htmlID()
|
||||||
|
session := resizable.Session()
|
||||||
|
column, row := resizable.cellSizeCSS()
|
||||||
|
|
||||||
|
updateCSSProperty(htmlID, "grid-template-columns", column, session)
|
||||||
|
updateCSSProperty(htmlID, "grid-template-rows", row, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) cellSizeCSS() (string, string) {
|
||||||
|
w := resizable.resizeBorderWidth().cssString("4px")
|
||||||
|
side := resizable.getSide()
|
||||||
|
column := "1fr"
|
||||||
|
row := "1fr"
|
||||||
|
|
||||||
|
if side&LeftSide != 0 {
|
||||||
|
if (side & RightSide) != 0 {
|
||||||
|
column = w + " 1fr " + w
|
||||||
|
} else {
|
||||||
|
column = w + " 1fr"
|
||||||
|
}
|
||||||
|
} else if (side & RightSide) != 0 {
|
||||||
|
column = "1fr " + w
|
||||||
|
}
|
||||||
|
|
||||||
|
if side&TopSide != 0 {
|
||||||
|
if (side & BottomSide) != 0 {
|
||||||
|
row = w + " 1fr " + w
|
||||||
|
} else {
|
||||||
|
row = w + " 1fr"
|
||||||
|
}
|
||||||
|
} else if (side & BottomSide) != 0 {
|
||||||
|
row = "1fr " + w
|
||||||
|
}
|
||||||
|
|
||||||
|
return column, row
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) cssStyle(self View, builder cssBuilder) {
|
||||||
|
column, row := resizable.cellSizeCSS()
|
||||||
|
|
||||||
|
builder.add("grid-template-columns", column)
|
||||||
|
builder.add("grid-template-rows", row)
|
||||||
|
|
||||||
|
resizable.viewData.cssStyle(self, builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
|
||||||
|
side := resizable.getSide()
|
||||||
|
left := 1
|
||||||
|
top := 1
|
||||||
|
leftSide := (side & LeftSide) != 0
|
||||||
|
rightSide := (side & RightSide) != 0
|
||||||
|
w := resizable.resizeBorderWidth().cssString("4px")
|
||||||
|
|
||||||
|
if leftSide {
|
||||||
|
left = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
writePos := func(x1, x2, y1, y2 int) {
|
||||||
|
buffer.WriteString(fmt.Sprintf(` grid-column-start: %d; grid-column-end: %d; grid-row-start: %d; grid-row-end: %d;"></div>`, x1, x2, y1, y2))
|
||||||
|
}
|
||||||
|
//htmlID := resizable.htmlID()
|
||||||
|
|
||||||
|
if (side & TopSide) != 0 {
|
||||||
|
top = 2
|
||||||
|
|
||||||
|
if leftSide {
|
||||||
|
buffer.WriteString(`<div onmousedown="startResize(this, -1, -1, event)" style="cursor: nwse-resize; width: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`; height: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`;`)
|
||||||
|
writePos(1, 2, 1, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`<div onmousedown="startResize(this, 0, -1, event)" style="cursor: ns-resize; width: 100%; height: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`;`)
|
||||||
|
writePos(left, left+1, 1, 2)
|
||||||
|
|
||||||
|
if rightSide {
|
||||||
|
buffer.WriteString(`<div onmousedown="startResize(this, 1, -1, event)" style="cursor: nesw-resize; width: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`; height: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`;`)
|
||||||
|
writePos(left+1, left+2, 1, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if leftSide {
|
||||||
|
buffer.WriteString(`<div onmousedown="startResize(this, -1, 0, event)" style="cursor: ew-resize; width: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`; height: 100%;`)
|
||||||
|
writePos(1, 2, top, top+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rightSide {
|
||||||
|
buffer.WriteString(`<div onmousedown="startResize(this, 1, 0, event)" style="cursor: ew-resize; width: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`; height: 100%;`)
|
||||||
|
writePos(left+1, left+2, top, top+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (side & BottomSide) != 0 {
|
||||||
|
if leftSide {
|
||||||
|
buffer.WriteString(`<div onmousedown="startResize(this, -1, 1, event)" style="cursor: nesw-resize; width: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`; height: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`;`)
|
||||||
|
writePos(1, 2, top+1, top+2)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`<div onmousedown="startResize(this, 0, 1, event)" style="cursor: ns-resize; width: 100%; height: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`;`)
|
||||||
|
writePos(left, left+1, top+1, top+2)
|
||||||
|
|
||||||
|
if rightSide {
|
||||||
|
buffer.WriteString(`<div onmousedown="startResize(this, 1, 1, event)" style="cursor: nwse-resize; width: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`; height: `)
|
||||||
|
buffer.WriteString(w)
|
||||||
|
buffer.WriteString(`;`)
|
||||||
|
writePos(left+1, left+2, top+1, top+2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resizable.content) > 0 {
|
||||||
|
view := resizable.content[0]
|
||||||
|
view.addToCSSStyle(map[string]string{
|
||||||
|
"grid-column-start": strconv.Itoa(left),
|
||||||
|
"grid-column-end": strconv.Itoa(left + 1),
|
||||||
|
"grid-row-start": strconv.Itoa(top),
|
||||||
|
"grid-row-end": strconv.Itoa(top + 1),
|
||||||
|
})
|
||||||
|
viewHTML(view, buffer)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,216 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
// ResizeEvent is the constant for "resize-event" property tag
|
||||||
|
// The "resize-event" is fired when the view changes its size.
|
||||||
|
// The main listener format: func(View, Frame).
|
||||||
|
// The additional listener formats: func(Frame), func(View), and func().
|
||||||
|
const ResizeEvent = "resize-event"
|
||||||
|
|
||||||
|
func (view *viewData) onResize(self View, x, y, width, height float64) {
|
||||||
|
view.frame.Left = x
|
||||||
|
view.frame.Top = y
|
||||||
|
view.frame.Width = width
|
||||||
|
view.frame.Height = height
|
||||||
|
for _, listener := range GetResizeListeners(view, "") {
|
||||||
|
listener(self, view.frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) onItemResize(self View, index int, x, y, width, height float64) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setFrameListener(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(View, Frame):
|
||||||
|
view.properties[tag] = []func(View, Frame){value}
|
||||||
|
|
||||||
|
case []func(View, Frame):
|
||||||
|
if len(value) > 0 {
|
||||||
|
view.properties[tag] = value
|
||||||
|
} else {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case func(Frame):
|
||||||
|
fn := func(view View, frame Frame) {
|
||||||
|
value(frame)
|
||||||
|
}
|
||||||
|
view.properties[tag] = []func(View, Frame){fn}
|
||||||
|
|
||||||
|
case []func(Frame):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners := make([]func(View, Frame), count)
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[i] = func(view View, frame Frame) {
|
||||||
|
val(frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
fn := func(view View, frame Frame) {
|
||||||
|
value(view)
|
||||||
|
}
|
||||||
|
view.properties[tag] = []func(View, Frame){fn}
|
||||||
|
|
||||||
|
case []func(View):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners := make([]func(View, Frame), count)
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[i] = func(view View, frame Frame) {
|
||||||
|
val(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
|
||||||
|
case func():
|
||||||
|
fn := func(view View, frame Frame) {
|
||||||
|
value()
|
||||||
|
}
|
||||||
|
view.properties[tag] = []func(View, Frame){fn}
|
||||||
|
|
||||||
|
case []func():
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners := make([]func(View, Frame), count)
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[i] = func(view View, frame Frame) {
|
||||||
|
val()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners := make([]func(View, Frame), count)
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := val.(type) {
|
||||||
|
case func(View, Frame):
|
||||||
|
listeners[i] = val
|
||||||
|
|
||||||
|
case func(Frame):
|
||||||
|
listeners[i] = func(view View, frame Frame) {
|
||||||
|
val(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
listeners[i] = func(view View, frame Frame) {
|
||||||
|
val(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func():
|
||||||
|
listeners[i] = func(view View, frame Frame) {
|
||||||
|
val()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setNoResizeEvent() {
|
||||||
|
view.noResizeEvent = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) isNoResizeEvent() bool {
|
||||||
|
return view.noResizeEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *viewsContainerData) isNoResizeEvent() bool {
|
||||||
|
if container.noResizeEvent {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if parent := container.Parent(); parent != nil {
|
||||||
|
return parent.isNoResizeEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Frame() Frame {
|
||||||
|
return view.frame
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetViewFrame returns the size and location of view's viewport.
|
||||||
|
// If the second argument (subviewID) is "" then the value of the first argument (view) is returned
|
||||||
|
func GetViewFrame(view View, subviewID string) Frame {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return Frame{}
|
||||||
|
}
|
||||||
|
return view.Frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResizeListeners returns the list of "resize-event" listeners. If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then the listeners list of the first argument (view) is returned
|
||||||
|
func GetResizeListeners(view View, subviewID string) []func(View, Frame) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(ResizeEvent); value != nil {
|
||||||
|
if result, ok := value.([]func(View, Frame)); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(View, Frame){}
|
||||||
|
}
|
|
@ -0,0 +1,418 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
imageDir = "images"
|
||||||
|
themeDir = "themes"
|
||||||
|
viewDir = "views"
|
||||||
|
rawDir = "raw"
|
||||||
|
stringsDir = "strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scaledImage struct {
|
||||||
|
path string
|
||||||
|
scale float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type imagePath struct {
|
||||||
|
path string
|
||||||
|
fs *embed.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceManager struct {
|
||||||
|
embedFS []*embed.FS
|
||||||
|
themes map[string]*theme
|
||||||
|
images map[string]imagePath
|
||||||
|
imageSrcSets map[string][]scaledImage
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
var resources = resourceManager{
|
||||||
|
embedFS: []*embed.FS{},
|
||||||
|
themes: map[string]*theme{},
|
||||||
|
images: map[string]imagePath{},
|
||||||
|
imageSrcSets: map[string][]scaledImage{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddEmbedResources(fs *embed.FS) {
|
||||||
|
resources.embedFS = append(resources.embedFS, fs)
|
||||||
|
rootDirs := embedRootDirs(fs)
|
||||||
|
for _, dir := range rootDirs {
|
||||||
|
switch dir {
|
||||||
|
case imageDir:
|
||||||
|
scanEmbedImagesDir(fs, dir, "")
|
||||||
|
|
||||||
|
case themeDir:
|
||||||
|
scanEmbedThemesDir(fs, dir)
|
||||||
|
|
||||||
|
case stringsDir:
|
||||||
|
scanEmbedStringsDir(fs, dir)
|
||||||
|
|
||||||
|
case viewDir, rawDir:
|
||||||
|
// do nothing
|
||||||
|
|
||||||
|
default:
|
||||||
|
if files, err := fs.ReadDir(dir); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
switch file.Name() {
|
||||||
|
case imageDir:
|
||||||
|
scanEmbedImagesDir(fs, dir+"/"+imageDir, "")
|
||||||
|
|
||||||
|
case themeDir:
|
||||||
|
scanEmbedThemesDir(fs, dir+"/"+themeDir)
|
||||||
|
|
||||||
|
case stringsDir:
|
||||||
|
scanEmbedStringsDir(fs, dir+"/"+stringsDir)
|
||||||
|
|
||||||
|
case viewDir, rawDir:
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func embedRootDirs(fs *embed.FS) []string {
|
||||||
|
result := []string{}
|
||||||
|
if files, err := fs.ReadDir("."); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
if file.IsDir() {
|
||||||
|
result = append(result, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanEmbedThemesDir(fs *embed.FS, dir string) {
|
||||||
|
if files, err := fs.ReadDir(dir); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
path := dir + "/" + name
|
||||||
|
if file.IsDir() {
|
||||||
|
scanEmbedThemesDir(fs, path)
|
||||||
|
} else if strings.ToLower(filepath.Ext(name)) == ".rui" {
|
||||||
|
if data, err := fs.ReadFile(path); err == nil {
|
||||||
|
RegisterThemeText(string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanEmbedImagesDir(fs *embed.FS, dir, prefix string) {
|
||||||
|
if files, err := fs.ReadDir(dir); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
path := dir + "/" + name
|
||||||
|
if file.IsDir() {
|
||||||
|
scanEmbedImagesDir(fs, path, prefix+name+"/")
|
||||||
|
} else {
|
||||||
|
ext := strings.ToLower(filepath.Ext(name))
|
||||||
|
switch ext {
|
||||||
|
case ".png", ".jpg", ".jpeg", ".svg":
|
||||||
|
registerImage(fs, path, prefix+name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidImageFileFormat(filename string) {
|
||||||
|
ErrorLog(`Invalid image file name parameters: "` + filename +
|
||||||
|
`". Image file name format: name[@x-param].ext (examples: icon.png, icon@1.5x.png)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerImage(fs *embed.FS, path, filename string) {
|
||||||
|
resources.images[filename] = imagePath{fs: fs, path: path}
|
||||||
|
|
||||||
|
start := strings.LastIndex(filename, "@")
|
||||||
|
if start < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := strings.LastIndex(filename, ".")
|
||||||
|
if start > ext || filename[ext-1] != 'x' {
|
||||||
|
invalidImageFileFormat(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if scale, err := strconv.ParseFloat(filename[start+1:ext-1], 32); err == nil {
|
||||||
|
key := filename[:start] + filename[ext:]
|
||||||
|
images, ok := resources.imageSrcSets[key]
|
||||||
|
if ok {
|
||||||
|
for _, image := range images {
|
||||||
|
if image.scale == scale {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
images = []scaledImage{}
|
||||||
|
}
|
||||||
|
resources.imageSrcSets[key] = append(images, scaledImage{path: filename, scale: scale})
|
||||||
|
} else {
|
||||||
|
invalidImageFileFormat(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanImagesDirectory(path, filePrefix string) {
|
||||||
|
if files, err := ioutil.ReadDir(path); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
filename := file.Name()
|
||||||
|
if filename[0] != '.' {
|
||||||
|
newPath := path + `/` + filename
|
||||||
|
if !file.IsDir() {
|
||||||
|
registerImage(nil, newPath, filePrefix+filename)
|
||||||
|
} else {
|
||||||
|
scanImagesDirectory(newPath, filePrefix+filename+"/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanThemesDir(path string) {
|
||||||
|
if files, err := ioutil.ReadDir(path); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
filename := file.Name()
|
||||||
|
if filename[0] != '.' {
|
||||||
|
newPath := path + `/` + filename
|
||||||
|
if file.IsDir() {
|
||||||
|
scanThemesDir(newPath)
|
||||||
|
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
|
||||||
|
if data, err := ioutil.ReadFile(newPath); err == nil {
|
||||||
|
RegisterThemeText(string(data))
|
||||||
|
} else {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetResourcePath set path of the resource directory
|
||||||
|
func SetResourcePath(path string) {
|
||||||
|
resources.path = path
|
||||||
|
pathLen := len(path)
|
||||||
|
if pathLen > 0 && path[pathLen-1] != '/' {
|
||||||
|
resources.path += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
scanImagesDirectory(resources.path+imageDir, "")
|
||||||
|
scanThemesDir(resources.path + themeDir)
|
||||||
|
scanStringsDir(resources.path + stringsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterThemeText parse text and add result to the theme list
|
||||||
|
func RegisterThemeText(text string) bool {
|
||||||
|
data := ParseDataText(text)
|
||||||
|
if data == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !data.IsObject() {
|
||||||
|
ErrorLog(`Root element is not object`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if data.Tag() != "theme" {
|
||||||
|
ErrorLog(`Invalid the root object tag. Must be "theme"`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if name, ok := data.PropertyValue("name"); ok && name != "" {
|
||||||
|
t := resources.themes[name]
|
||||||
|
if t == nil {
|
||||||
|
t = new(theme)
|
||||||
|
t.init()
|
||||||
|
resources.themes[name] = t
|
||||||
|
}
|
||||||
|
t.addData(data)
|
||||||
|
} else {
|
||||||
|
defaultTheme.addData(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request) bool {
|
||||||
|
serveEmbed := func(fs *embed.FS, path string) bool {
|
||||||
|
if file, err := fs.Open(path); err == nil {
|
||||||
|
if stat, err := file.Stat(); err == nil {
|
||||||
|
http.ServeContent(w, r, filename, stat.ModTime(), file.(io.ReadSeeker))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if image, ok := resources.images[filename]; ok {
|
||||||
|
if image.fs != nil {
|
||||||
|
if serveEmbed(image.fs, image.path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := os.Stat(image.path); err == nil {
|
||||||
|
http.ServeFile(w, r, image.path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fs := range resources.embedFS {
|
||||||
|
if serveEmbed(fs, filename) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, dir := range embedRootDirs(fs) {
|
||||||
|
if serveEmbed(fs, dir+"/"+filename) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if subdirs, err := fs.ReadDir(dir); err == nil {
|
||||||
|
for _, subdir := range subdirs {
|
||||||
|
if subdir.IsDir() {
|
||||||
|
if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serve := func(path, filename string) bool {
|
||||||
|
filepath := path + filename
|
||||||
|
if _, err := os.Stat(filepath); err == nil {
|
||||||
|
http.ServeFile(w, r, filepath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath = path + imageDir + "/" + filename
|
||||||
|
if _, err := os.Stat(filepath); err == nil {
|
||||||
|
http.ServeFile(w, r, filepath)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if resources.path != "" && serve(resources.path, filename) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if exe, err := os.Executable(); err == nil {
|
||||||
|
path := filepath.Dir(exe) + "/resources/"
|
||||||
|
if serve(path, filename) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadRawResource(filename string) []byte {
|
||||||
|
for _, fs := range resources.embedFS {
|
||||||
|
rootDirs := embedRootDirs(fs)
|
||||||
|
for _, dir := range rootDirs {
|
||||||
|
switch dir {
|
||||||
|
case imageDir, themeDir, viewDir:
|
||||||
|
// do nothing
|
||||||
|
|
||||||
|
case rawDir:
|
||||||
|
if data, err := fs.ReadFile(dir + "/" + filename); err == nil {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if data, err := fs.ReadFile(dir + "/" + rawDir + "/" + filename); err == nil {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readFile := func(path string) []byte {
|
||||||
|
if data, err := os.ReadFile(resources.path + rawDir + "/" + filename); err == nil {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resources.path != "" {
|
||||||
|
if data := readFile(resources.path + rawDir + "/" + filename); data != nil {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exe, err := os.Executable(); err == nil {
|
||||||
|
if data := readFile(filepath.Dir(exe) + "/resources/" + rawDir + "/" + filename); data != nil {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLogF(`The raw file "%s" don't found`, filename)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AllRawResources() []string {
|
||||||
|
result := []string{}
|
||||||
|
|
||||||
|
for _, fs := range resources.embedFS {
|
||||||
|
rootDirs := embedRootDirs(fs)
|
||||||
|
for _, dir := range rootDirs {
|
||||||
|
switch dir {
|
||||||
|
case imageDir, themeDir, viewDir:
|
||||||
|
// do nothing
|
||||||
|
|
||||||
|
case rawDir:
|
||||||
|
if files, err := fs.ReadDir(rawDir); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
result = append(result, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if files, err := fs.ReadDir(dir + "/" + rawDir); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
result = append(result, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resources.path != "" {
|
||||||
|
if files, err := ioutil.ReadDir(resources.path + rawDir); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
result = append(result, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exe, err := os.Executable(); err == nil {
|
||||||
|
if files, err := ioutil.ReadDir(filepath.Dir(exe) + "/resources/" + rawDir); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
result = append(result, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ruiWriter interface {
|
||||||
|
startObject(tag string)
|
||||||
|
startObjectProperty(tag, objectTag string)
|
||||||
|
endObject()
|
||||||
|
writeProperty(tag string, value interface{})
|
||||||
|
finish() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ruiStringer interface {
|
||||||
|
ruiString(writer ruiWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ruiWriterData struct {
|
||||||
|
buffer *strings.Builder
|
||||||
|
indent string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRUIWriter() ruiWriter {
|
||||||
|
writer := new(ruiWriterData)
|
||||||
|
return writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *ruiWriterData) writeIndent() {
|
||||||
|
if writer.buffer == nil {
|
||||||
|
writer.buffer = allocStringBuilder()
|
||||||
|
writer.indent = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if writer.indent != "" {
|
||||||
|
writer.buffer.WriteString(writer.indent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *ruiWriterData) writeString(str string) {
|
||||||
|
esc := map[string]string{"\t": `\t`, "\r": `\r`, "\n": `\n`, "\"": `"`}
|
||||||
|
hasEsc := false
|
||||||
|
for s := range esc {
|
||||||
|
if strings.Contains(str, s) {
|
||||||
|
hasEsc = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasEsc || strings.Contains(str, " ") || strings.Contains(str, ",") {
|
||||||
|
if !strings.Contains(str, "`") && (hasEsc || strings.Contains(str, `\`)) {
|
||||||
|
writer.buffer.WriteRune('`')
|
||||||
|
writer.buffer.WriteString(str)
|
||||||
|
writer.buffer.WriteRune('`')
|
||||||
|
} else {
|
||||||
|
str = strings.Replace(str, `\`, `\\`, -1)
|
||||||
|
for oldStr, newStr := range esc {
|
||||||
|
str = strings.Replace(str, oldStr, newStr, -1)
|
||||||
|
}
|
||||||
|
writer.buffer.WriteRune('"')
|
||||||
|
writer.buffer.WriteString(str)
|
||||||
|
writer.buffer.WriteRune('"')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writer.buffer.WriteString(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *ruiWriterData) startObject(tag string) {
|
||||||
|
writer.writeIndent()
|
||||||
|
writer.indent += "\t"
|
||||||
|
writer.writeString(tag)
|
||||||
|
writer.buffer.WriteString(" {\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *ruiWriterData) startObjectProperty(tag, objectTag string) {
|
||||||
|
writer.writeIndent()
|
||||||
|
writer.indent += "\t"
|
||||||
|
writer.writeString(tag)
|
||||||
|
writer.writeString(" = ")
|
||||||
|
writer.writeString(objectTag)
|
||||||
|
writer.buffer.WriteString(" {\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *ruiWriterData) endObject() {
|
||||||
|
if len(writer.indent) > 0 {
|
||||||
|
writer.indent = writer.indent[1:]
|
||||||
|
}
|
||||||
|
writer.writeIndent()
|
||||||
|
writer.buffer.WriteRune('}')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *ruiWriterData) writeValue(value interface{}) {
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
writer.writeString(value)
|
||||||
|
|
||||||
|
case ruiStringer:
|
||||||
|
value.ruiString(writer)
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
writer.writeString(value.String())
|
||||||
|
|
||||||
|
case float32:
|
||||||
|
writer.writeString(fmt.Sprintf("%g", float64(value)))
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
writer.writeString(fmt.Sprintf("%g", value))
|
||||||
|
|
||||||
|
case []string:
|
||||||
|
switch len(value) {
|
||||||
|
case 0:
|
||||||
|
writer.buffer.WriteString("[]\n")
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
writer.writeString(value[0])
|
||||||
|
|
||||||
|
default:
|
||||||
|
writer.buffer.WriteString("[\n")
|
||||||
|
writer.indent += "\t"
|
||||||
|
for _, v := range value {
|
||||||
|
writer.buffer.WriteString(writer.indent)
|
||||||
|
writer.writeString(v)
|
||||||
|
writer.buffer.WriteString(",\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.indent = writer.indent[1:]
|
||||||
|
writer.buffer.WriteString(writer.indent)
|
||||||
|
writer.buffer.WriteRune(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
case []View:
|
||||||
|
switch len(value) {
|
||||||
|
case 0:
|
||||||
|
writer.buffer.WriteString("[]\n")
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
writer.writeValue(value[0])
|
||||||
|
|
||||||
|
default:
|
||||||
|
writer.buffer.WriteString("[\n")
|
||||||
|
writer.indent += "\t"
|
||||||
|
for _, v := range value {
|
||||||
|
writer.buffer.WriteString(writer.indent)
|
||||||
|
v.ruiString(writer)
|
||||||
|
writer.buffer.WriteString(",\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.indent = writer.indent[1:]
|
||||||
|
writer.buffer.WriteString(writer.indent)
|
||||||
|
writer.buffer.WriteRune(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
switch len(value) {
|
||||||
|
case 0:
|
||||||
|
writer.buffer.WriteString("[]\n")
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
writer.writeValue(value[0])
|
||||||
|
|
||||||
|
default:
|
||||||
|
writer.buffer.WriteString("[\n")
|
||||||
|
writer.indent += "\t"
|
||||||
|
for _, v := range value {
|
||||||
|
writer.buffer.WriteString(writer.indent)
|
||||||
|
writer.writeValue(v)
|
||||||
|
writer.buffer.WriteString(",\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.indent = writer.indent[1:]
|
||||||
|
writer.buffer.WriteString(writer.indent)
|
||||||
|
writer.buffer.WriteRune(']')
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
writer.buffer.WriteString(strconv.Itoa(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.buffer.WriteString(",\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *ruiWriterData) writeProperty(tag string, value interface{}) {
|
||||||
|
writer.writeIndent()
|
||||||
|
writer.writeString(tag)
|
||||||
|
writer.buffer.WriteString(" = ")
|
||||||
|
writer.writeValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *ruiWriterData) finish() string {
|
||||||
|
result := ""
|
||||||
|
if writer.buffer != nil {
|
||||||
|
result = writer.buffer.String()
|
||||||
|
freeStringBuilder(writer.buffer)
|
||||||
|
writer.buffer = nil
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// ScrollEvent is the constant for "scroll-event" property tag
|
||||||
|
// The "resize-event" is fired when the content of the view is scrolled.
|
||||||
|
// The main listener format: func(View, Frame).
|
||||||
|
// The additional listener formats: func(Frame), func(View), and func().
|
||||||
|
const ScrollEvent = "scroll-event"
|
||||||
|
|
||||||
|
func (view *viewData) onScroll(self View, x, y, width, height float64) {
|
||||||
|
view.scroll.Left = x
|
||||||
|
view.scroll.Top = y
|
||||||
|
view.scroll.Width = width
|
||||||
|
view.scroll.Height = height
|
||||||
|
for _, listener := range GetScrollListeners(view, "") {
|
||||||
|
listener(self, view.scroll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Scroll() Frame {
|
||||||
|
return view.scroll
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setScroll(x, y, width, height float64) {
|
||||||
|
view.scroll.Left = x
|
||||||
|
view.scroll.Top = y
|
||||||
|
view.scroll.Width = width
|
||||||
|
view.scroll.Height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetViewScroll returns ...
|
||||||
|
// If the second argument (subviewID) is "" then a value of the first argument (view) is returned
|
||||||
|
func GetViewScroll(view View, subviewID string) Frame {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return Frame{}
|
||||||
|
}
|
||||||
|
return view.Scroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetScrollListeners returns the list of "scroll-event" listeners. If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then the listeners list of the first argument (view) is returned
|
||||||
|
func GetScrollListeners(view View, subviewID string) []func(View, Frame) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(ScrollEvent); value != nil {
|
||||||
|
if result, ok := value.([]func(View, Frame)); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(View, Frame){}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollTo scrolls the view's content to the given position.
|
||||||
|
// If the second argument (subviewID) is "" then the first argument (view) is used
|
||||||
|
func ScrollViewTo(view View, subviewID string, x, y float64) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
view.Session().runScript(fmt.Sprintf(`scrollTo("%s", %g, %g)`, view.htmlID(), x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollViewToEnd scrolls the view's content to the start of view.
|
||||||
|
// If the second argument (subviewID) is "" then the first argument (view) is used
|
||||||
|
func ScrollViewToStart(view View, subviewID string) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
view.Session().runScript(`scrollToStart("` + view.htmlID() + `")`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollViewToEnd scrolls the view's content to the end of view.
|
||||||
|
// If the second argument (subviewID) is "" then the first argument (view) is used
|
||||||
|
func ScrollViewToEnd(view View, subviewID string) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
view.Session().runScript(`scrollToEnd("` + view.htmlID() + `")`)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,401 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionContent is the interface of a session content
|
||||||
|
type SessionContent interface {
|
||||||
|
CreateRootView(session Session) View
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session provide interface to session parameters assess
|
||||||
|
type Session interface {
|
||||||
|
// App return the current application interface
|
||||||
|
App() Application
|
||||||
|
// ID return the id of the session
|
||||||
|
ID() int
|
||||||
|
|
||||||
|
// DarkTheme returns "true" if the dark theme is used
|
||||||
|
DarkTheme() bool
|
||||||
|
// Mobile returns "true" if current session is displayed on a touch screen device
|
||||||
|
TouchScreen() bool
|
||||||
|
// PixelRatio returns the ratio of the resolution in physical pixels to the resolution
|
||||||
|
// in logical pixels for the current display device.
|
||||||
|
PixelRatio() float64
|
||||||
|
// TextDirection returns the default text direction (LeftToRightDirection (1) or RightToLeftDirection (2))
|
||||||
|
TextDirection() int
|
||||||
|
// Constant returns the constant with "tag" name or "" if it is not exists
|
||||||
|
Constant(tag string) (string, bool)
|
||||||
|
// Color returns the color with "tag" name or 0 if it is not exists
|
||||||
|
Color(tag string) (Color, bool)
|
||||||
|
// SetCustomTheme set the custom theme
|
||||||
|
SetCustomTheme(name string) bool
|
||||||
|
// Language returns the current session language
|
||||||
|
Language() string
|
||||||
|
// SetLanguage set the current session language
|
||||||
|
SetLanguage(lang string)
|
||||||
|
// GetString returns the text for the current language
|
||||||
|
GetString(tag string) (string, bool)
|
||||||
|
|
||||||
|
Content() SessionContent
|
||||||
|
setContent(content SessionContent, self Session) bool
|
||||||
|
|
||||||
|
// RootView returns the root view of the session
|
||||||
|
RootView() View
|
||||||
|
// Get returns a value of the view (with id defined by the first argument) property with name defined by the second argument.
|
||||||
|
// The type of return value depends on the property. If the property is not set then nil is returned.
|
||||||
|
Get(viewID, tag string) interface{}
|
||||||
|
// Set sets the value (third argument) of the property (second argument) of the view with id defined by the first argument.
|
||||||
|
// Return "true" if the value has been set, in the opposite case "false" are returned and
|
||||||
|
// a description of the error is written to the log
|
||||||
|
Set(viewID, tag string, value interface{}) bool
|
||||||
|
|
||||||
|
resolveConstants(value string) (string, bool)
|
||||||
|
checkboxOffImage() string
|
||||||
|
checkboxOnImage() string
|
||||||
|
radiobuttonOffImage() string
|
||||||
|
radiobuttonOnImage() string
|
||||||
|
|
||||||
|
viewByHTMLID(id string) View
|
||||||
|
nextViewID() string
|
||||||
|
styleProperty(styleTag, property string) (string, bool)
|
||||||
|
stylePropertyNode(styleTag, propertyTag string) DataNode
|
||||||
|
|
||||||
|
setBrige(events chan DataObject, brige WebBrige)
|
||||||
|
writeInitScript(writer *strings.Builder)
|
||||||
|
runScript(script string)
|
||||||
|
runGetterScript(script string) DataObject //, answer chan DataObject)
|
||||||
|
handleAnswer(data DataObject)
|
||||||
|
handleResize(data DataObject)
|
||||||
|
handleViewEvent(command string, data DataObject)
|
||||||
|
close()
|
||||||
|
|
||||||
|
onStart()
|
||||||
|
onFinish()
|
||||||
|
onPause()
|
||||||
|
onResume()
|
||||||
|
onDisconnect()
|
||||||
|
onReconnect()
|
||||||
|
|
||||||
|
ignoreViewUpdates() bool
|
||||||
|
setIgnoreViewUpdates(ignore bool)
|
||||||
|
|
||||||
|
popupManager() *popupManager
|
||||||
|
imageManager() *imageManager
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionData struct {
|
||||||
|
customTheme *theme
|
||||||
|
darkTheme bool
|
||||||
|
touchScreen bool
|
||||||
|
textDirection int
|
||||||
|
pixelRatio float64
|
||||||
|
language string
|
||||||
|
languages []string
|
||||||
|
checkboxOff string
|
||||||
|
checkboxOn string
|
||||||
|
radiobuttonOff string
|
||||||
|
radiobuttonOn string
|
||||||
|
app Application
|
||||||
|
sessionID int
|
||||||
|
viewCounter int
|
||||||
|
content SessionContent
|
||||||
|
rootView View
|
||||||
|
ignoreUpdates bool
|
||||||
|
popups *popupManager
|
||||||
|
images *imageManager
|
||||||
|
brige WebBrige
|
||||||
|
events chan DataObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSession(app Application, id int, customTheme string, params DataObject) Session {
|
||||||
|
session := new(sessionData)
|
||||||
|
session.app = app
|
||||||
|
session.sessionID = id
|
||||||
|
session.darkTheme = false
|
||||||
|
session.touchScreen = false
|
||||||
|
session.pixelRatio = 1
|
||||||
|
session.textDirection = LeftToRightDirection
|
||||||
|
session.languages = []string{}
|
||||||
|
session.viewCounter = 0
|
||||||
|
session.ignoreUpdates = false
|
||||||
|
|
||||||
|
if customTheme != "" {
|
||||||
|
if theme, ok := newTheme(customTheme); ok {
|
||||||
|
session.customTheme = theme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := params.PropertyValue("touch"); ok {
|
||||||
|
session.touchScreen = (value == "1" || value == "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := params.PropertyValue("direction"); ok {
|
||||||
|
if value == "rtl" {
|
||||||
|
session.textDirection = RightToLeftDirection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := params.PropertyValue("languages"); ok {
|
||||||
|
session.languages = strings.Split(value, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := params.PropertyValue("dark"); ok {
|
||||||
|
session.darkTheme = (value == "1" || value == "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := params.PropertyValue("pixel-ratio"); ok {
|
||||||
|
if f, err := strconv.ParseFloat(value, 64); err != nil {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
} else {
|
||||||
|
session.pixelRatio = f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) App() Application {
|
||||||
|
return session.app
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) ID() int {
|
||||||
|
return session.sessionID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) setBrige(events chan DataObject, brige WebBrige) {
|
||||||
|
session.events = events
|
||||||
|
session.brige = brige
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) close() {
|
||||||
|
if session.events != nil {
|
||||||
|
session.events <- ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) styleProperty(styleTag, propertyTag string) (string, bool) {
|
||||||
|
var style DataObject
|
||||||
|
ok := false
|
||||||
|
if session.customTheme != nil {
|
||||||
|
style, ok = session.customTheme.styles[styleTag]
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
style, ok = defaultTheme.styles[styleTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
if node := style.PropertyWithTag(propertyTag); node != nil && node.Type() == TextNode {
|
||||||
|
return session.resolveConstants(node.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//errorLogF(`property "%v" not found`, propertyTag)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) stylePropertyNode(styleTag, propertyTag string) DataNode {
|
||||||
|
var style DataObject
|
||||||
|
ok := false
|
||||||
|
if session.customTheme != nil {
|
||||||
|
style, ok = session.customTheme.styles[styleTag]
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
style, ok = defaultTheme.styles[styleTag]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return style.PropertyWithTag(propertyTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) nextViewID() string {
|
||||||
|
session.viewCounter++
|
||||||
|
return fmt.Sprintf("id%06d", session.viewCounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) viewByHTMLID(id string) View {
|
||||||
|
if session.rootView == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
popupManager := session.popupManager()
|
||||||
|
for _, popup := range popupManager.popups {
|
||||||
|
if view := popup.viewByHTMLID(id); view != nil {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return viewByHTMLID(id, session.rootView)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) Content() SessionContent {
|
||||||
|
return session.content
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) setContent(content SessionContent, self Session) bool {
|
||||||
|
if content != nil {
|
||||||
|
session.content = content
|
||||||
|
session.rootView = content.CreateRootView(self)
|
||||||
|
if session.rootView != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) RootView() View {
|
||||||
|
return session.rootView
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) writeInitScript(writer *strings.Builder) {
|
||||||
|
var workTheme *theme
|
||||||
|
if session.customTheme == nil {
|
||||||
|
workTheme = defaultTheme
|
||||||
|
} else {
|
||||||
|
workTheme = new(theme)
|
||||||
|
workTheme.init()
|
||||||
|
workTheme.concat(defaultTheme)
|
||||||
|
workTheme.concat(session.customTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if css := workTheme.cssText(session); css != "" {
|
||||||
|
writer.WriteString(`document.querySelector('style').textContent += "`)
|
||||||
|
writer.WriteString(css)
|
||||||
|
writer.WriteString("\";\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.rootView != nil {
|
||||||
|
writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
|
||||||
|
viewHTML(session.rootView, writer)
|
||||||
|
writer.WriteString("';\nscanElementsSize();")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) reload() {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
session.writeInitScript(buffer)
|
||||||
|
session.runScript(buffer.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) ignoreViewUpdates() bool {
|
||||||
|
return session.brige == nil || session.ignoreUpdates
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) setIgnoreViewUpdates(ignore bool) {
|
||||||
|
session.ignoreUpdates = ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) Get(viewID, tag string) interface{} {
|
||||||
|
if view := ViewByID(session.RootView(), viewID); view != nil {
|
||||||
|
return view.Get(tag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) Set(viewID, tag string, value interface{}) bool {
|
||||||
|
if view := ViewByID(session.RootView(), viewID); view != nil {
|
||||||
|
return view.Set(tag, value)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) popupManager() *popupManager {
|
||||||
|
if session.popups == nil {
|
||||||
|
session.popups = new(popupManager)
|
||||||
|
session.popups.popups = []Popup{}
|
||||||
|
}
|
||||||
|
return session.popups
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) imageManager() *imageManager {
|
||||||
|
if session.images == nil {
|
||||||
|
session.images = new(imageManager)
|
||||||
|
session.images.images = make(map[string]*imageData)
|
||||||
|
}
|
||||||
|
return session.images
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) runScript(script string) {
|
||||||
|
if session.brige != nil {
|
||||||
|
session.brige.WriteMessage(script)
|
||||||
|
} else {
|
||||||
|
ErrorLog("No connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) {
|
||||||
|
if session.brige != nil {
|
||||||
|
return session.brige.RunGetterScript(script)
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog("No connection")
|
||||||
|
result := NewDataObject("error")
|
||||||
|
result.SetPropertyValue("text", "No connection")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) handleAnswer(data DataObject) {
|
||||||
|
session.brige.AnswerReceived(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) handleResize(data DataObject) {
|
||||||
|
if node := data.PropertyWithTag("views"); node != nil && node.Type() == ArrayNode {
|
||||||
|
for _, el := range node.ArrayElements() {
|
||||||
|
if el.IsObject() {
|
||||||
|
obj := el.Object()
|
||||||
|
getFloat := func(tag string) float64 {
|
||||||
|
if value, ok := obj.PropertyValue(tag); ok {
|
||||||
|
f, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err == nil {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
ErrorLog(`Resize event error: ` + err.Error())
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`Resize event error: the property "%s" not found`, tag)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if viewID, ok := obj.PropertyValue("id"); ok {
|
||||||
|
if n := strings.IndexRune(viewID, '-'); n > 0 {
|
||||||
|
if index, err := strconv.Atoi(viewID[n+1:]); err == nil {
|
||||||
|
if view := session.viewByHTMLID(viewID[:n]); view != nil {
|
||||||
|
view.onItemResize(view, index, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height"))
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`View with id == %s not found`, viewID[:n])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`Invalid view id == %s not found`, viewID)
|
||||||
|
}
|
||||||
|
} else if view := session.viewByHTMLID(viewID); view != nil {
|
||||||
|
view.onResize(view, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height"))
|
||||||
|
view.setScroll(getFloat("scroll-x"), getFloat("scroll-y"), getFloat("scroll-width"), getFloat("scroll-height"))
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`View with id == %s not found`, viewID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(`"id" property not found`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(`Resize event error: views element is not object`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(`Resize event error: invalid "views" property`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) handleViewEvent(command string, data DataObject) {
|
||||||
|
if viewID, ok := data.PropertyValue("id"); ok {
|
||||||
|
if view := session.viewByHTMLID(viewID); view != nil {
|
||||||
|
view.handleCommand(view, command, data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(`"id" property not found. Event: ` + command)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
// SessionStartListener is the listener interface of a session start event
|
||||||
|
type SessionStartListener interface {
|
||||||
|
OnStart(session Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionFinishListener is the listener interface of a session start event
|
||||||
|
type SessionFinishListener interface {
|
||||||
|
OnFinish(session Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionResumeListener is the listener interface of a session resume event
|
||||||
|
type SessionResumeListener interface {
|
||||||
|
OnResume(session Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPauseListener is the listener interface of a session pause event
|
||||||
|
type SessionPauseListener interface {
|
||||||
|
OnPause(session Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPauseListener is the listener interface of a session disconnect event
|
||||||
|
type SessionDisconnectListener interface {
|
||||||
|
OnDisconnect(session Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPauseListener is the listener interface of a session reconnect event
|
||||||
|
type SessionReconnectListener interface {
|
||||||
|
OnReconnect(session Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) onStart() {
|
||||||
|
if session.content != nil {
|
||||||
|
if listener, ok := session.content.(SessionStartListener); ok {
|
||||||
|
listener.OnStart(session)
|
||||||
|
}
|
||||||
|
session.onResume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) onFinish() {
|
||||||
|
if session.content != nil {
|
||||||
|
session.onPause()
|
||||||
|
if listener, ok := session.content.(SessionFinishListener); ok {
|
||||||
|
listener.OnFinish(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) onPause() {
|
||||||
|
if session.content != nil {
|
||||||
|
if listener, ok := session.content.(SessionPauseListener); ok {
|
||||||
|
listener.OnPause(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) onResume() {
|
||||||
|
if session.content != nil {
|
||||||
|
if listener, ok := session.content.(SessionResumeListener); ok {
|
||||||
|
listener.OnResume(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) onDisconnect() {
|
||||||
|
if session.content != nil {
|
||||||
|
if listener, ok := session.content.(SessionDisconnectListener); ok {
|
||||||
|
listener.OnDisconnect(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) onReconnect() {
|
||||||
|
if session.content != nil {
|
||||||
|
if listener, ok := session.content.(SessionReconnectListener); ok {
|
||||||
|
listener.OnReconnect(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,359 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
type Session struct {
|
||||||
|
customTheme *theme
|
||||||
|
darkTheme bool
|
||||||
|
touchScreen bool
|
||||||
|
textDirection int
|
||||||
|
pixelRatio float64
|
||||||
|
language string
|
||||||
|
languages []string
|
||||||
|
checkboxOff string
|
||||||
|
checkboxOn string
|
||||||
|
radiobuttonOff string
|
||||||
|
radiobuttonOn string
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
func (session *sessionData) DarkTheme() bool {
|
||||||
|
return session.darkTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) TouchScreen() bool {
|
||||||
|
return session.touchScreen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) PixelRatio() float64 {
|
||||||
|
return session.pixelRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) TextDirection() int {
|
||||||
|
return session.textDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) constant(tag string, prevTags []string) (string, bool) {
|
||||||
|
tags := append(prevTags, tag)
|
||||||
|
result := ""
|
||||||
|
themes := session.themes()
|
||||||
|
for {
|
||||||
|
ok := false
|
||||||
|
if session.touchScreen {
|
||||||
|
for _, theme := range themes {
|
||||||
|
if theme.touchConstants != nil {
|
||||||
|
if result, ok = theme.touchConstants[tag]; ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
for _, theme := range themes {
|
||||||
|
if theme.constants != nil {
|
||||||
|
if result, ok = theme.constants[tag]; ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ErrorLogF(`"%v" constant not found`, tag)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) < 2 || !strings.ContainsRune(result, '@') {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, separator := range []string{",", " ", ":", ";", "|", "/"} {
|
||||||
|
if strings.Contains(result, separator) {
|
||||||
|
result, ok = session.resolveConstantsNext(result, tags)
|
||||||
|
return result, ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result[0] != '@' {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = result[1:]
|
||||||
|
for _, t := range tags {
|
||||||
|
if t == tag {
|
||||||
|
ErrorLogF(`"%v" constant is cyclic`, tag)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) resolveConstants(value string) (string, bool) {
|
||||||
|
return session.resolveConstantsNext(value, []string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) resolveConstantsNext(value string, prevTags []string) (string, bool) {
|
||||||
|
if !strings.Contains(value, "@") {
|
||||||
|
return value, true
|
||||||
|
}
|
||||||
|
|
||||||
|
separators := []rune{',', ' ', ':', ';', '|', '/'}
|
||||||
|
sep := rune(0)
|
||||||
|
index := -1
|
||||||
|
for _, s := range separators {
|
||||||
|
if i := strings.IndexRune(value, s); i >= 0 {
|
||||||
|
if i < index || index < 0 {
|
||||||
|
sep = s
|
||||||
|
index = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := true
|
||||||
|
if index >= 0 {
|
||||||
|
v1 := strings.Trim(value[:index], " \t\n\r")
|
||||||
|
v2 := strings.Trim(value[index+1:], " \t\n\r")
|
||||||
|
if len(v1) > 1 && v1[0] == '@' {
|
||||||
|
if v1, ok = session.constant(v1[1:], prevTags); !ok {
|
||||||
|
return value, false
|
||||||
|
}
|
||||||
|
if v, ok := session.resolveConstantsNext(v1, prevTags); ok {
|
||||||
|
v1 = v
|
||||||
|
} else {
|
||||||
|
return v1 + string(sep) + v2, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := session.resolveConstantsNext(v2, prevTags); ok {
|
||||||
|
v2 = v
|
||||||
|
}
|
||||||
|
|
||||||
|
return v1 + string(sep) + v2, ok
|
||||||
|
|
||||||
|
} else if value[0] == '@' {
|
||||||
|
|
||||||
|
if value, ok = session.constant(value[1:], prevTags); ok {
|
||||||
|
return session.resolveConstantsNext(value, prevTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) Constant(tag string) (string, bool) {
|
||||||
|
return session.constant(tag, []string{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) themes() []*theme {
|
||||||
|
if session.customTheme != nil {
|
||||||
|
return []*theme{session.customTheme, defaultTheme}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []*theme{defaultTheme}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color return the color with "tag" name or 0 if it is not exists
|
||||||
|
func (session *sessionData) Color(tag string) (Color, bool) {
|
||||||
|
tags := []string{tag}
|
||||||
|
result := ""
|
||||||
|
themes := session.themes()
|
||||||
|
for {
|
||||||
|
ok := false
|
||||||
|
if session.darkTheme {
|
||||||
|
for _, theme := range themes {
|
||||||
|
if theme.darkColors != nil {
|
||||||
|
if result, ok = theme.darkColors[tag]; ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
for _, theme := range themes {
|
||||||
|
if theme.colors != nil {
|
||||||
|
if result, ok = theme.colors[tag]; ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
ErrorLogF(`"%v" color not found`, tag)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 || result[0] != '@' {
|
||||||
|
color, ok := StringToColor(result)
|
||||||
|
if !ok {
|
||||||
|
ErrorLogF(`invalid value "%v" of "%v" color constant`, result, tag)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return color, true
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = result[1:]
|
||||||
|
for _, t := range tags {
|
||||||
|
if t == tag {
|
||||||
|
ErrorLogF(`"%v" color is cyclic`, tag)
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags = append(tags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) SetCustomTheme(name string) bool {
|
||||||
|
if name == "" {
|
||||||
|
if session.customTheme == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else if theme, ok := resources.themes[name]; ok {
|
||||||
|
session.customTheme = theme
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
session.reload()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkImage = `<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m4 8 3 4 5-8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"/></svg>`
|
||||||
|
|
||||||
|
func (session *sessionData) checkboxImage(checked bool) string {
|
||||||
|
|
||||||
|
var borderColor, backgroundColor Color
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if borderColor, ok = session.Color("ruiDisabledTextColor"); !ok {
|
||||||
|
if session.darkTheme {
|
||||||
|
borderColor = 0xFFA0A0A0
|
||||||
|
} else {
|
||||||
|
borderColor = 0xFF202020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if checked {
|
||||||
|
if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
|
||||||
|
backgroundColor = 0xFF1A74E8
|
||||||
|
}
|
||||||
|
} else if backgroundColor, ok = session.Color("backgroundColor"); !ok {
|
||||||
|
if session.darkTheme {
|
||||||
|
backgroundColor = 0xFFA0A0A0
|
||||||
|
} else {
|
||||||
|
backgroundColor = 0xFF202020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(`<div style="width: 18px; height: 18px; background-color: `)
|
||||||
|
buffer.WriteString(backgroundColor.cssString())
|
||||||
|
buffer.WriteString(`; border: 1px solid `)
|
||||||
|
buffer.WriteString(borderColor.cssString())
|
||||||
|
buffer.WriteString(`; border-radius: 4px;">`)
|
||||||
|
if checked {
|
||||||
|
buffer.WriteString(checkImage)
|
||||||
|
}
|
||||||
|
buffer.WriteString(`</div>`)
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) checkboxOffImage() string {
|
||||||
|
if session.checkboxOff == "" {
|
||||||
|
session.checkboxOff = session.checkboxImage(false)
|
||||||
|
}
|
||||||
|
return session.checkboxOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) checkboxOnImage() string {
|
||||||
|
if session.checkboxOn == "" {
|
||||||
|
session.checkboxOn = session.checkboxImage(true)
|
||||||
|
}
|
||||||
|
return session.checkboxOn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) radiobuttonOffImage() string {
|
||||||
|
if session.radiobuttonOff == "" {
|
||||||
|
var borderColor, backgroundColor Color
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if borderColor, ok = session.Color("ruiDisabledTextColor"); !ok {
|
||||||
|
if session.darkTheme {
|
||||||
|
borderColor = 0xFFA0A0A0
|
||||||
|
} else {
|
||||||
|
borderColor = 0xFF202020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if backgroundColor, ok = session.Color("backgroundColor"); !ok {
|
||||||
|
if session.darkTheme {
|
||||||
|
backgroundColor = 0xFFA0A0A0
|
||||||
|
} else {
|
||||||
|
backgroundColor = 0xFF202020
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session.radiobuttonOff = fmt.Sprintf(`<div style="width: 16px; height: 16px; background-color: %s; border: 1px solid %s; border-radius: 8px;"></div>`,
|
||||||
|
backgroundColor.cssString(), borderColor.cssString())
|
||||||
|
}
|
||||||
|
return session.radiobuttonOff
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) radiobuttonOnImage() string {
|
||||||
|
if session.radiobuttonOn == "" {
|
||||||
|
var borderColor, backgroundColor Color
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
if borderColor, ok = session.Color("ruiHighlightColor"); !ok {
|
||||||
|
borderColor = 0xFF1A74E8
|
||||||
|
}
|
||||||
|
|
||||||
|
if backgroundColor, ok = session.Color("ruiHighlightTextColor"); !ok {
|
||||||
|
backgroundColor = 0xFFFFFFFF
|
||||||
|
}
|
||||||
|
|
||||||
|
session.radiobuttonOn = fmt.Sprintf(`<div style="width: 16px; height: 16px; display: grid; justify-items: center; align-items: center; background-color: %s; border: 2px solid %s; border-radius: 8px;"><div style="width: 8px; height: 8px; background-color: %s; border-radius: 4px;"></div></div>`,
|
||||||
|
backgroundColor.cssString(), borderColor.cssString(), borderColor.cssString())
|
||||||
|
}
|
||||||
|
return session.radiobuttonOn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) Language() string {
|
||||||
|
if session.language != "" {
|
||||||
|
return session.language
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.languages != nil && len(session.languages) > 0 {
|
||||||
|
return session.languages[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return "en"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) SetLanguage(lang string) {
|
||||||
|
lang = strings.Trim(lang, " \t\n\r")
|
||||||
|
if lang != session.language {
|
||||||
|
session.language = lang
|
||||||
|
|
||||||
|
if session.rootView != nil {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
|
||||||
|
viewHTML(session.rootView, buffer)
|
||||||
|
buffer.WriteString("';\nscanElementsSize();")
|
||||||
|
|
||||||
|
session.runScript(buffer.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sizeConstant(session Session, tag string) (SizeUnit, bool) {
|
||||||
|
if text, ok := session.Constant(tag); ok {
|
||||||
|
return StringToSizeUnit(text)
|
||||||
|
}
|
||||||
|
return AutoSize(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCSSStyle(htmlID string, session Session) {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
if view := session.viewByHTMLID(htmlID); view != nil {
|
||||||
|
var builder viewCSSBuilder
|
||||||
|
|
||||||
|
builder.buffer = allocStringBuilder()
|
||||||
|
builder.buffer.WriteString(`updateCSSStyle('`)
|
||||||
|
builder.buffer.WriteString(view.htmlID())
|
||||||
|
builder.buffer.WriteString(`', '`)
|
||||||
|
view.cssStyle(view, &builder)
|
||||||
|
builder.buffer.WriteString(`');`)
|
||||||
|
view.Session().runScript(builder.finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateInnerHTML(htmlID string, session Session) {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
if view := session.viewByHTMLID(htmlID); view != nil {
|
||||||
|
script := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(script)
|
||||||
|
|
||||||
|
script.Grow(32 * 1024)
|
||||||
|
view.htmlSubviews(view, script)
|
||||||
|
view.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, view.htmlID(), script.String()))
|
||||||
|
//view.updateEventHandlers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendToInnerHTML(htmlID, content string, session Session) {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
if view := session.viewByHTMLID(htmlID); view != nil {
|
||||||
|
view.Session().runScript(fmt.Sprintf(`appendToInnerHTML('%v', '%v');`, view.htmlID(), content))
|
||||||
|
//view.updateEventHandlers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateProperty(htmlID, property, value string, session Session) {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', '%v');`, htmlID, property, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateCSSProperty(htmlID, property, value string, session Session) {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
session.runScript(fmt.Sprintf(`updateCSSProperty('%v', '%v', '%v');`, htmlID, property, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateBoolProperty(htmlID, property string, value bool, session Session) {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
if value {
|
||||||
|
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', true);`, htmlID, property))
|
||||||
|
} else {
|
||||||
|
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', false);`, htmlID, property))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeProperty(htmlID, property string, session Session) {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
session.runScript(fmt.Sprintf(`removeProperty('%v', '%v');`, htmlID, property))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func setDisabled(htmlID string, disabled bool, session Session) {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
if disabled {
|
||||||
|
session.runScript(fmt.Sprintf(`setDisabled('%v', true);`, htmlID))
|
||||||
|
} else {
|
||||||
|
session.runScript(fmt.Sprintf(`setDisabled('%v', false);`, htmlID))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func viewByHTMLID(id string, startView View) View {
|
||||||
|
if startView != nil {
|
||||||
|
if startView.htmlID() == id {
|
||||||
|
return startView
|
||||||
|
}
|
||||||
|
if container, ok := startView.(ParanetView); ok {
|
||||||
|
for _, view := range container.Views() {
|
||||||
|
if view != nil {
|
||||||
|
if v := viewByHTMLID(id, view); v != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,122 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stopTestLogFlag = false
|
||||||
|
var testLogDone chan int
|
||||||
|
var ignoreTestLog = false
|
||||||
|
|
||||||
|
func createTestLog(t *testing.T, ignore bool) {
|
||||||
|
ignoreTestLog = ignore
|
||||||
|
SetErrorLog(func(text string) {
|
||||||
|
if ignoreTestLog {
|
||||||
|
t.Log(text)
|
||||||
|
} else {
|
||||||
|
t.Error(text)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
SetDebugLog(func(text string) {
|
||||||
|
t.Log(text)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func createTestSession(t *testing.T) *sessionData {
|
||||||
|
session := new(sessionData)
|
||||||
|
createTestLog(t, false)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionConstants(t *testing.T) {
|
||||||
|
session := createTestSession(t)
|
||||||
|
|
||||||
|
customTheme := `
|
||||||
|
theme {
|
||||||
|
colors = _{
|
||||||
|
textColor = #FF080808,
|
||||||
|
myColor = #81234567
|
||||||
|
},
|
||||||
|
colors:dark = _{
|
||||||
|
textColor = #FFF0F0F0,
|
||||||
|
myColor = #87654321
|
||||||
|
},
|
||||||
|
constants = _{
|
||||||
|
defaultPadding = 10px,
|
||||||
|
myConstant = 100%
|
||||||
|
const1 = "@const2, 10px; @const3"
|
||||||
|
const2 = "20mm / @const4"
|
||||||
|
const3 = "@const5 : 30pt"
|
||||||
|
const4 = "40%"
|
||||||
|
const5 = "50px"
|
||||||
|
},
|
||||||
|
constants:touch = _{
|
||||||
|
defaultPadding = 20px,
|
||||||
|
myConstant = 80%,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
SetErrorLog(func(text string) {
|
||||||
|
t.Error(text)
|
||||||
|
})
|
||||||
|
|
||||||
|
theme, ok := newTheme(customTheme)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.SetCustomTheme(theme)
|
||||||
|
|
||||||
|
type constPair struct {
|
||||||
|
tag, value string
|
||||||
|
}
|
||||||
|
|
||||||
|
testConstants := func(constants []constPair) {
|
||||||
|
for _, constant := range constants {
|
||||||
|
if value, ok := session.Constant(constant.tag); ok {
|
||||||
|
if value != constant.value {
|
||||||
|
t.Error(constant.tag + " = " + value + ". Need: " + constant.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testConstants([]constPair{
|
||||||
|
{tag: "defaultPadding", value: "10px"},
|
||||||
|
{tag: "myConstant", value: "100%"},
|
||||||
|
{tag: "buttonMargin", value: "4px"},
|
||||||
|
})
|
||||||
|
|
||||||
|
session.SetConstant("myConstant", "25px")
|
||||||
|
|
||||||
|
testConstants([]constPair{
|
||||||
|
{tag: "defaultPadding", value: "10px"},
|
||||||
|
{tag: "myConstant", value: "25px"},
|
||||||
|
{tag: "buttonMargin", value: "4px"},
|
||||||
|
})
|
||||||
|
|
||||||
|
session.touchScreen = true
|
||||||
|
|
||||||
|
testConstants([]constPair{
|
||||||
|
{tag: "defaultPadding", value: "20px"},
|
||||||
|
{tag: "myConstant", value: "80%"},
|
||||||
|
{tag: "buttonMargin", value: "4px"},
|
||||||
|
})
|
||||||
|
|
||||||
|
session.SetTouchConstant("myConstant", "30pt")
|
||||||
|
|
||||||
|
testConstants([]constPair{
|
||||||
|
{tag: "defaultPadding", value: "20px"},
|
||||||
|
{tag: "myConstant", value: "30pt"},
|
||||||
|
{tag: "buttonMargin", value: "4px"},
|
||||||
|
})
|
||||||
|
|
||||||
|
if value, ok := session.Constant("const1"); ok {
|
||||||
|
if value != "20mm/40%,10px;50px:30pt" {
|
||||||
|
t.Error("const1 = " + value + ". Need: 20mm/40%,10px;50px:30pt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,312 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ColorProperty is the name of the color property of the shadow.
|
||||||
|
ColorProperty = "color"
|
||||||
|
// Inset is the name of bool property of the shadow. If it is set to "false" (default) then the shadow
|
||||||
|
// is assumed to be a drop shadow (as if the box were raised above the content).
|
||||||
|
// If it is set to "true" then the shadow to one inside the frame (as if the content was depressed inside the box).
|
||||||
|
// Inset shadows are drawn inside the border (even transparent ones), above the background, but below content.
|
||||||
|
Inset = "inset"
|
||||||
|
// XOffset is the name of the SizeUnit property of the shadow that determines the shadow horizontal offset.
|
||||||
|
// Negative values place the shadow to the left of the element.
|
||||||
|
XOffset = "x-offset"
|
||||||
|
// YOffset is the name of the SizeUnit property of the shadow that determines the shadow vertical offset.
|
||||||
|
// Negative values place the shadow above the element.
|
||||||
|
YOffset = "y-offset"
|
||||||
|
// BlurRadius is the name of the SizeUnit property of the shadow that determines the radius of the blur effect.
|
||||||
|
// The larger this value, the bigger the blur, so the shadow becomes bigger and lighter. Negative values are not allowed.
|
||||||
|
BlurRadius = "blur"
|
||||||
|
// SpreadRadius is the name of the SizeUnit property of the shadow. Positive values will cause the shadow to expand
|
||||||
|
// and grow bigger, negative values will cause the shadow to shrink.
|
||||||
|
SpreadRadius = "spread-radius"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ViewShadow contains attributes of the view shadow
|
||||||
|
type ViewShadow interface {
|
||||||
|
Properties
|
||||||
|
fmt.Stringer
|
||||||
|
ruiStringer
|
||||||
|
cssStyle(buffer *strings.Builder, session Session, lead string) bool
|
||||||
|
cssTextStyle(buffer *strings.Builder, session Session, lead string) bool
|
||||||
|
visible(session Session) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type viewShadowData struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewViewShadow create the new shadow for a view. Arguments:
|
||||||
|
// offsetX, offsetY - x and y offset of the shadow
|
||||||
|
// blurRadius - the blur radius of the shadow
|
||||||
|
// spreadRadius - the spread radius of the shadow
|
||||||
|
// color - the color of the shadow
|
||||||
|
func NewViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow {
|
||||||
|
return NewShadowWithParams(Params{
|
||||||
|
XOffset: offsetX,
|
||||||
|
YOffset: offsetY,
|
||||||
|
BlurRadius: blurRadius,
|
||||||
|
SpreadRadius: spreadRadius,
|
||||||
|
ColorProperty: color,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewInsetViewShadow create the new inset shadow for a view. Arguments:
|
||||||
|
// offsetX, offsetY - x and y offset of the shadow
|
||||||
|
// blurRadius - the blur radius of the shadow
|
||||||
|
// spreadRadius - the spread radius of the shadow
|
||||||
|
// color - the color of the shadow
|
||||||
|
func NewInsetViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow {
|
||||||
|
return NewShadowWithParams(Params{
|
||||||
|
XOffset: offsetX,
|
||||||
|
YOffset: offsetY,
|
||||||
|
BlurRadius: blurRadius,
|
||||||
|
SpreadRadius: spreadRadius,
|
||||||
|
ColorProperty: color,
|
||||||
|
Inset: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextShadow create the new text shadow. Arguments:
|
||||||
|
// offsetX, offsetY - x and y offset of the shadow
|
||||||
|
// blurRadius - the blur radius of the shadow
|
||||||
|
// color - the color of the shadow
|
||||||
|
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow {
|
||||||
|
return NewShadowWithParams(Params{
|
||||||
|
XOffset: offsetX,
|
||||||
|
YOffset: offsetY,
|
||||||
|
BlurRadius: blurRadius,
|
||||||
|
ColorProperty: color,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewShadowWithParams create the new shadow for a view.
|
||||||
|
func NewShadowWithParams(params Params) ViewShadow {
|
||||||
|
shadow := new(viewShadowData)
|
||||||
|
shadow.propertyList.init()
|
||||||
|
if params != nil {
|
||||||
|
for _, tag := range []string{ColorProperty, Inset, XOffset, YOffset, BlurRadius, SpreadRadius} {
|
||||||
|
if value, ok := params[tag]; ok && value != nil {
|
||||||
|
shadow.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseViewShadow parse DataObject and create ViewShadow object
|
||||||
|
func parseViewShadow(object DataObject) ViewShadow {
|
||||||
|
shadow := new(viewShadowData)
|
||||||
|
shadow.propertyList.init()
|
||||||
|
parseProperties(shadow, object)
|
||||||
|
return shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shadow *viewShadowData) Remove(tag string) {
|
||||||
|
delete(shadow.properties, strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shadow *viewShadowData) Set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
shadow.Remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case ColorProperty, Inset, XOffset, YOffset, BlurRadius, SpreadRadius:
|
||||||
|
return shadow.propertyList.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLogF(`"%s" property is not supported by Shadow`, tag)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shadow *viewShadowData) Get(tag string) interface{} {
|
||||||
|
return shadow.propertyList.Get(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shadow *viewShadowData) cssStyle(buffer *strings.Builder, session Session, lead string) bool {
|
||||||
|
color, _ := colorProperty(shadow, ColorProperty, session)
|
||||||
|
offsetX, _ := sizeProperty(shadow, XOffset, session)
|
||||||
|
offsetY, _ := sizeProperty(shadow, YOffset, session)
|
||||||
|
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
|
||||||
|
spreadRadius, _ := sizeProperty(shadow, SpreadRadius, session)
|
||||||
|
|
||||||
|
if color.Alpha() == 0 ||
|
||||||
|
((offsetX.Type == Auto || offsetX.Value == 0) &&
|
||||||
|
(offsetY.Type == Auto || offsetY.Value == 0) &&
|
||||||
|
(blurRadius.Type == Auto || blurRadius.Value == 0) &&
|
||||||
|
(spreadRadius.Type == Auto || spreadRadius.Value == 0)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(lead)
|
||||||
|
if inset, _ := boolProperty(shadow, Inset, session); inset {
|
||||||
|
buffer.WriteString("inset ")
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(offsetX.cssString("0"))
|
||||||
|
buffer.WriteByte(' ')
|
||||||
|
buffer.WriteString(offsetY.cssString("0"))
|
||||||
|
buffer.WriteByte(' ')
|
||||||
|
buffer.WriteString(blurRadius.cssString("0"))
|
||||||
|
buffer.WriteByte(' ')
|
||||||
|
buffer.WriteString(spreadRadius.cssString("0"))
|
||||||
|
buffer.WriteByte(' ')
|
||||||
|
buffer.WriteString(color.cssString())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shadow *viewShadowData) cssTextStyle(buffer *strings.Builder, session Session, lead string) bool {
|
||||||
|
color, _ := colorProperty(shadow, ColorProperty, session)
|
||||||
|
offsetX, _ := sizeProperty(shadow, XOffset, session)
|
||||||
|
offsetY, _ := sizeProperty(shadow, YOffset, session)
|
||||||
|
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
|
||||||
|
|
||||||
|
if color.Alpha() == 0 ||
|
||||||
|
((offsetX.Type == Auto || offsetX.Value == 0) &&
|
||||||
|
(offsetY.Type == Auto || offsetY.Value == 0) &&
|
||||||
|
(blurRadius.Type == Auto || blurRadius.Value == 0)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(lead)
|
||||||
|
buffer.WriteString(offsetX.cssString("0"))
|
||||||
|
buffer.WriteByte(' ')
|
||||||
|
buffer.WriteString(offsetY.cssString("0"))
|
||||||
|
buffer.WriteByte(' ')
|
||||||
|
buffer.WriteString(blurRadius.cssString("0"))
|
||||||
|
buffer.WriteByte(' ')
|
||||||
|
buffer.WriteString(color.cssString())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shadow *viewShadowData) visible(session Session) bool {
|
||||||
|
color, _ := colorProperty(shadow, ColorProperty, session)
|
||||||
|
offsetX, _ := sizeProperty(shadow, XOffset, session)
|
||||||
|
offsetY, _ := sizeProperty(shadow, YOffset, session)
|
||||||
|
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
|
||||||
|
spreadRadius, _ := sizeProperty(shadow, SpreadRadius, session)
|
||||||
|
|
||||||
|
if color.Alpha() == 0 ||
|
||||||
|
((offsetX.Type == Auto || offsetX.Value == 0) &&
|
||||||
|
(offsetY.Type == Auto || offsetY.Value == 0) &&
|
||||||
|
(blurRadius.Type == Auto || blurRadius.Value == 0) &&
|
||||||
|
(spreadRadius.Type == Auto || spreadRadius.Value == 0)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shadow *viewShadowData) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
shadow.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (shadow *viewShadowData) ruiString(writer ruiWriter) {
|
||||||
|
writer.startObject("_")
|
||||||
|
for _, tag := range shadow.AllTags() {
|
||||||
|
if value := shadow.Get(tag); value != nil {
|
||||||
|
writer.writeProperty(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (properties *propertyList) setShadow(tag string, value interface{}) bool {
|
||||||
|
|
||||||
|
if value == nil {
|
||||||
|
delete(properties.properties, tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case ViewShadow:
|
||||||
|
properties.properties[tag] = []ViewShadow{value}
|
||||||
|
|
||||||
|
case []ViewShadow:
|
||||||
|
if len(value) == 0 {
|
||||||
|
delete(properties.properties, tag)
|
||||||
|
} else {
|
||||||
|
properties.properties[tag] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataValue:
|
||||||
|
if !value.IsObject() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
properties.properties[tag] = []ViewShadow{parseViewShadow(value.Object())}
|
||||||
|
|
||||||
|
case []DataValue:
|
||||||
|
shadows := []ViewShadow{}
|
||||||
|
for _, data := range value {
|
||||||
|
if data.IsObject() {
|
||||||
|
shadows = append(shadows, parseViewShadow(data.Object()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(shadows) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
properties.properties[tag] = shadows
|
||||||
|
|
||||||
|
case string:
|
||||||
|
obj := NewDataObject(value)
|
||||||
|
if obj == nil {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
properties.properties[tag] = []ViewShadow{parseViewShadow(obj)}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getShadows(properties Properties, tag string) []ViewShadow {
|
||||||
|
if value := properties.Get(tag); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case []ViewShadow:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case ViewShadow:
|
||||||
|
return []ViewShadow{value}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []ViewShadow{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shadowCSS(properties Properties, tag string, session Session) string {
|
||||||
|
shadows := getShadows(properties, tag)
|
||||||
|
if len(shadows) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
lead := ""
|
||||||
|
if tag == Shadow {
|
||||||
|
for _, shadow := range shadows {
|
||||||
|
if shadow.cssStyle(buffer, session, lead) {
|
||||||
|
lead = ", "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, shadow := range shadows {
|
||||||
|
if shadow.cssTextStyle(buffer, session, lead) {
|
||||||
|
lead = ", "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SizeUnitType : type of enumerated constants for define a type of SizeUnit value.
|
||||||
|
//
|
||||||
|
// Can take the following values: Auto, SizeInPixel, SizeInPercent,
|
||||||
|
// SizeInDIP, SizeInPt, SizeInInch, SizeInMM, SizeInFraction
|
||||||
|
type SizeUnitType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Auto - default value.
|
||||||
|
Auto SizeUnitType = 0
|
||||||
|
// SizeInPixel - size in pixels.
|
||||||
|
SizeInPixel SizeUnitType = 1
|
||||||
|
// SizeInEM - size in em.
|
||||||
|
SizeInEM SizeUnitType = 2
|
||||||
|
// SizeInEX - size in em.
|
||||||
|
SizeInEX SizeUnitType = 3
|
||||||
|
// SizeInPercent - size in percents of a parant size.
|
||||||
|
SizeInPercent SizeUnitType = 4
|
||||||
|
// SizeInPt - size in pt (1/72 inch).
|
||||||
|
SizeInPt SizeUnitType = 5
|
||||||
|
// SizeInPc - size in pc (1pc = 12pt).
|
||||||
|
SizeInPc SizeUnitType = 6
|
||||||
|
// SizeInInch - size in inches.
|
||||||
|
SizeInInch SizeUnitType = 7
|
||||||
|
// SizeInMM - size in millimeters.
|
||||||
|
SizeInMM SizeUnitType = 8
|
||||||
|
// SizeInCM - size in centimeters.
|
||||||
|
SizeInCM SizeUnitType = 9
|
||||||
|
// SizeInFraction - size in fraction. Used only for "cell-width" and "cell-height" property
|
||||||
|
SizeInFraction SizeUnitType = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
// SizeUnit describe a size (Value field) and size unit (Type field).
|
||||||
|
type SizeUnit struct {
|
||||||
|
Type SizeUnitType
|
||||||
|
Value float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoSize creates SizeUnit with Auto type
|
||||||
|
func AutoSize() SizeUnit {
|
||||||
|
return SizeUnit{Auto, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Px creates SizeUnit with SizeInPixel type
|
||||||
|
func Px(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInPixel, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Em creates SizeUnit with SizeInEM type
|
||||||
|
func Em(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInEM, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ex creates SizeUnit with SizeInEX type
|
||||||
|
func Ex(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInEX, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Percent creates SizeUnit with SizeInDIP type
|
||||||
|
func Percent(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInPercent, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pt creates SizeUnit with SizeInPt type
|
||||||
|
func Pt(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInPt, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pc creates SizeUnit with SizeInPc type
|
||||||
|
func Pc(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInPc, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mm creates SizeUnit with SizeInMM type
|
||||||
|
func Mm(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInMM, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cm creates SizeUnit with SizeInCM type
|
||||||
|
func Cm(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInCM, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inch creates SizeUnit with SizeInInch type
|
||||||
|
func Inch(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInInch, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fr creates SizeUnit with SizeInFraction type
|
||||||
|
func Fr(value float64) SizeUnit {
|
||||||
|
return SizeUnit{SizeInFraction, value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal compare two SizeUnit. Return true if SizeUnit are equal
|
||||||
|
func (size SizeUnit) Equal(size2 SizeUnit) bool {
|
||||||
|
return size.Type == size2.Type && (size.Type == Auto || size.Value == size2.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeUnitSuffixes() map[SizeUnitType]string {
|
||||||
|
return map[SizeUnitType]string{
|
||||||
|
SizeInPixel: "px",
|
||||||
|
SizeInPercent: "%",
|
||||||
|
SizeInEM: "em",
|
||||||
|
SizeInEX: "ex",
|
||||||
|
SizeInPt: "pt",
|
||||||
|
SizeInPc: "pc",
|
||||||
|
SizeInInch: "in",
|
||||||
|
SizeInMM: "mm",
|
||||||
|
SizeInCM: "cm",
|
||||||
|
SizeInFraction: "fr",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringToSizeUnit converts the string argument to SizeUnit
|
||||||
|
func StringToSizeUnit(value string) (SizeUnit, bool) {
|
||||||
|
|
||||||
|
value = strings.Trim(value, " \t\n\r")
|
||||||
|
|
||||||
|
switch value {
|
||||||
|
case "auto", "none", "":
|
||||||
|
return SizeUnit{Type: Auto, Value: 0}, true
|
||||||
|
|
||||||
|
case "0":
|
||||||
|
return SizeUnit{Type: SizeInPixel, Value: 0}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
suffixes := sizeUnitSuffixes()
|
||||||
|
for unitType, suffix := range suffixes {
|
||||||
|
if strings.HasSuffix(value, suffix) {
|
||||||
|
var err error
|
||||||
|
var val float64
|
||||||
|
if val, err = strconv.ParseFloat(value[:len(value)-len(suffix)], 64); err != nil {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
return SizeUnit{Type: Auto, Value: 0}, false
|
||||||
|
}
|
||||||
|
return SizeUnit{Type: unitType, Value: val}, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog(`Invalid SizeUnit value: "` + value + `"`)
|
||||||
|
return SizeUnit{Type: Auto, Value: 0}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// String - convert SizeUnit to string
|
||||||
|
func (size SizeUnit) String() string {
|
||||||
|
if size.Type == Auto {
|
||||||
|
return "auto"
|
||||||
|
}
|
||||||
|
if suffix, ok := sizeUnitSuffixes()[size.Type]; ok {
|
||||||
|
return fmt.Sprintf("%g%s", size.Value, suffix)
|
||||||
|
}
|
||||||
|
return strconv.FormatFloat(size.Value, 'g', -1, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cssString - convert SizeUnit to string
|
||||||
|
func (size SizeUnit) cssString(textForAuto string) string {
|
||||||
|
switch size.Type {
|
||||||
|
case Auto:
|
||||||
|
return textForAuto
|
||||||
|
|
||||||
|
case SizeInEM:
|
||||||
|
return fmt.Sprintf("%grem", size.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if size.Value == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
return size.String()
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
/*
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSizeUnitNew(t *testing.T) {
|
||||||
|
_ = createTestSession(t)
|
||||||
|
size := SizeUnit{SizeInPixel, 10}
|
||||||
|
if Px(10) != size {
|
||||||
|
t.Error("Px(10) error")
|
||||||
|
}
|
||||||
|
|
||||||
|
size = SizeUnit{SizeInPercent, 10}
|
||||||
|
if Percent(10) != size {
|
||||||
|
t.Error("Percent(10) error")
|
||||||
|
}
|
||||||
|
|
||||||
|
size = SizeUnit{SizeInPt, 10}
|
||||||
|
if Pt(10) != size {
|
||||||
|
t.Error("Pt(10) error")
|
||||||
|
}
|
||||||
|
|
||||||
|
size = SizeUnit{SizeInCM, 10}
|
||||||
|
if Cm(10) != size {
|
||||||
|
t.Error("Dip(10) error")
|
||||||
|
}
|
||||||
|
|
||||||
|
size = SizeUnit{SizeInMM, 10}
|
||||||
|
if Mm(10) != size {
|
||||||
|
t.Error("Mm(10) error")
|
||||||
|
}
|
||||||
|
|
||||||
|
size = SizeUnit{SizeInInch, 10}
|
||||||
|
if Inch(10) != size {
|
||||||
|
t.Error("Inch(10) error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSizeUnitSet(t *testing.T) {
|
||||||
|
_ = createTestSession(t)
|
||||||
|
|
||||||
|
obj := new(dataObject)
|
||||||
|
obj.SetPropertyValue("x", "20")
|
||||||
|
obj.SetPropertyValue("size", "10mm")
|
||||||
|
|
||||||
|
size := SizeUnit{Auto, 0}
|
||||||
|
if size.setProperty(obj, "size", new(sessionData), nil) && (size.Type != SizeInMM || size.Value != 10) {
|
||||||
|
t.Errorf("result: Type = %d, Value = %g", size.Type, size.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSizeUnitSetValue(t *testing.T) {
|
||||||
|
_ = createTestSession(t)
|
||||||
|
|
||||||
|
type testData struct {
|
||||||
|
text string
|
||||||
|
size SizeUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
testValues := []testData{
|
||||||
|
testData{"auto", SizeUnit{Auto, 0}},
|
||||||
|
testData{"1.5em", SizeUnit{SizeInEM, 1.5}},
|
||||||
|
testData{"2ex", SizeUnit{SizeInEX, 2}},
|
||||||
|
testData{"20px", SizeUnit{SizeInPixel, 20}},
|
||||||
|
testData{"100%", SizeUnit{SizeInPercent, 100}},
|
||||||
|
testData{"14pt", SizeUnit{SizeInPt, 14}},
|
||||||
|
testData{"10pc", SizeUnit{SizeInPc, 10}},
|
||||||
|
testData{"0.1in", SizeUnit{SizeInInch, 0.1}},
|
||||||
|
testData{"10mm", SizeUnit{SizeInMM, 10}},
|
||||||
|
testData{"90.5cm", SizeUnit{SizeInCM, 90.5}},
|
||||||
|
}
|
||||||
|
|
||||||
|
var size SizeUnit
|
||||||
|
for _, data := range testValues {
|
||||||
|
if size.SetValue(data.text) && size != data.size {
|
||||||
|
t.Errorf("set \"%s\" result: Type = %d, Value = %g", data.text, size.Type, size.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
failValues := []string{
|
||||||
|
"xxx",
|
||||||
|
"10.10.10px",
|
||||||
|
"1000",
|
||||||
|
"5km",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, text := range failValues {
|
||||||
|
size.SetValue(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSizeUnitWriteData(t *testing.T) {
|
||||||
|
_ = createTestSession(t)
|
||||||
|
type testData struct {
|
||||||
|
text string
|
||||||
|
size SizeUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
testValues := []testData{
|
||||||
|
testData{"auto", SizeUnit{Auto, 0}},
|
||||||
|
testData{"1.5em", SizeUnit{SizeInEM, 1.5}},
|
||||||
|
testData{"2ex", SizeUnit{SizeInEX, 2}},
|
||||||
|
testData{"20px", SizeUnit{SizeInPixel, 20}},
|
||||||
|
testData{"100%", SizeUnit{SizeInPercent, 100}},
|
||||||
|
testData{"14pt", SizeUnit{SizeInPt, 14}},
|
||||||
|
testData{"10pc", SizeUnit{SizeInPc, 10}},
|
||||||
|
testData{"0.1in", SizeUnit{SizeInInch, 0.1}},
|
||||||
|
testData{"10mm", SizeUnit{SizeInMM, 10}},
|
||||||
|
testData{"90.5cm", SizeUnit{SizeInCM, 90.5}},
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
for _, data := range testValues {
|
||||||
|
buffer.Reset()
|
||||||
|
buffer.WriteString(data.size.String())
|
||||||
|
str := buffer.String()
|
||||||
|
if str != data.text {
|
||||||
|
t.Errorf("result: \"%s\", expected: \"%s\"", str, data.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,290 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// DefaultAnimation - default animation of StackLayout push
|
||||||
|
DefaultAnimation = 0
|
||||||
|
// StartToEndAnimation - start to end animation of StackLayout push
|
||||||
|
StartToEndAnimation = 1
|
||||||
|
// EndToStartAnimation - end to start animation of StackLayout push
|
||||||
|
EndToStartAnimation = 2
|
||||||
|
// TopDownAnimation - top down animation of StackLayout push
|
||||||
|
TopDownAnimation = 3
|
||||||
|
// BottomUpAnimation - bottom up animation of StackLayout push
|
||||||
|
BottomUpAnimation = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// StackLayout - list-container of View
|
||||||
|
type StackLayout interface {
|
||||||
|
ViewsContainer
|
||||||
|
Peek() View
|
||||||
|
MoveToFront(view View) bool
|
||||||
|
MoveToFrontByID(viewID string) bool
|
||||||
|
Push(view View, animation int, onPushFinished func())
|
||||||
|
Pop(animation int, onPopFinished func(View)) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type stackLayoutData struct {
|
||||||
|
viewsContainerData
|
||||||
|
peek uint
|
||||||
|
pushView, popView View
|
||||||
|
animationType int
|
||||||
|
onPushFinished func()
|
||||||
|
onPopFinished func(View)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStackLayout create new StackLayout object and return it
|
||||||
|
func NewStackLayout(session Session, params Params) StackLayout {
|
||||||
|
view := new(stackLayoutData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStackLayout(session Session) View {
|
||||||
|
return NewStackLayout(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of ViewsContainer by default values
|
||||||
|
func (layout *stackLayoutData) Init(session Session) {
|
||||||
|
layout.viewsContainerData.Init(session)
|
||||||
|
layout.tag = "StackLayout"
|
||||||
|
layout.systemClass = "ruiStackLayout"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) OnAnimationFinished(view View, tag string) {
|
||||||
|
switch tag {
|
||||||
|
case "ruiPush":
|
||||||
|
if layout.pushView != nil {
|
||||||
|
layout.pushView = nil
|
||||||
|
count := len(layout.views)
|
||||||
|
if count > 0 {
|
||||||
|
layout.peek = uint(count - 1)
|
||||||
|
} else {
|
||||||
|
layout.peek = 0
|
||||||
|
}
|
||||||
|
updateInnerHTML(layout.htmlID(), layout.session)
|
||||||
|
}
|
||||||
|
if layout.onPushFinished != nil {
|
||||||
|
onPushFinished := layout.onPushFinished
|
||||||
|
layout.onPushFinished = nil
|
||||||
|
onPushFinished()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "ruiPop":
|
||||||
|
popView := layout.popView
|
||||||
|
layout.popView = nil
|
||||||
|
updateInnerHTML(layout.htmlID(), layout.session)
|
||||||
|
if layout.onPopFinished != nil {
|
||||||
|
onPopFinished := layout.onPopFinished
|
||||||
|
layout.onPopFinished = nil
|
||||||
|
onPopFinished(popView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) Peek() View {
|
||||||
|
if int(layout.peek) < len(layout.views) {
|
||||||
|
return layout.views[layout.peek]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) MoveToFront(view View) bool {
|
||||||
|
peek := int(layout.peek)
|
||||||
|
htmlID := view.htmlID()
|
||||||
|
for i, view2 := range layout.views {
|
||||||
|
if view2.htmlID() == htmlID {
|
||||||
|
if i != peek {
|
||||||
|
if peek < len(layout.views) {
|
||||||
|
updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden", layout.Session())
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.peek = uint(i)
|
||||||
|
updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible", layout.Session())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog(`MoveToFront() fail. Subview not found."`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) MoveToFrontByID(viewID string) bool {
|
||||||
|
peek := int(layout.peek)
|
||||||
|
for i, view := range layout.views {
|
||||||
|
if view.ID() == viewID {
|
||||||
|
if i != peek {
|
||||||
|
if peek < len(layout.views) {
|
||||||
|
updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden", layout.Session())
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.peek = uint(i)
|
||||||
|
updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible", layout.Session())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLogF(`MoveToFront("%s") fail. Subview with "%s" not found."`, viewID, viewID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) Append(view View) {
|
||||||
|
if view != nil {
|
||||||
|
layout.peek = uint(len(layout.views))
|
||||||
|
layout.viewsContainerData.Append(view)
|
||||||
|
} else {
|
||||||
|
ErrorLog("StackLayout.Append(nil, ....) is forbidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) Insert(view View, index uint) {
|
||||||
|
if view != nil {
|
||||||
|
count := uint(len(layout.views))
|
||||||
|
if index < count {
|
||||||
|
layout.peek = index
|
||||||
|
} else {
|
||||||
|
layout.peek = count
|
||||||
|
}
|
||||||
|
layout.viewsContainerData.Insert(view, index)
|
||||||
|
} else {
|
||||||
|
ErrorLog("StackLayout.Insert(nil, ....) is forbidden")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) RemoveView(index uint) View {
|
||||||
|
if layout.peek > 0 {
|
||||||
|
layout.peek--
|
||||||
|
}
|
||||||
|
return layout.viewsContainerData.RemoveView(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) {
|
||||||
|
if view == nil {
|
||||||
|
ErrorLog("StackLayout.Push(nil, ....) is forbidden")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.pushView = view
|
||||||
|
layout.animationType = animation
|
||||||
|
layout.animation["ruiPush"] = Animation{FinishListener: layout}
|
||||||
|
layout.onPushFinished = onPushFinished
|
||||||
|
|
||||||
|
htmlID := layout.htmlID()
|
||||||
|
session := layout.Session()
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(`<div id="`)
|
||||||
|
buffer.WriteString(htmlID)
|
||||||
|
buffer.WriteString(`push" class="ruiStackPageLayout" ontransitionend="stackTransitionEndEvent(\'`)
|
||||||
|
buffer.WriteString(htmlID)
|
||||||
|
buffer.WriteString(`\', \'ruiPush\', event)" style="`)
|
||||||
|
|
||||||
|
switch layout.animationType {
|
||||||
|
case StartToEndAnimation:
|
||||||
|
buffer.WriteString(fmt.Sprintf("transform: translate(-%gpx, 0px); transition: transform ", layout.frame.Width))
|
||||||
|
|
||||||
|
case TopDownAnimation:
|
||||||
|
buffer.WriteString(fmt.Sprintf("transform: translate(0px, -%gpx); transition: transform ", layout.frame.Height))
|
||||||
|
|
||||||
|
case BottomUpAnimation:
|
||||||
|
buffer.WriteString(fmt.Sprintf("transform: translate(0px, %gpx); transition: transform ", layout.frame.Height))
|
||||||
|
|
||||||
|
default:
|
||||||
|
buffer.WriteString(fmt.Sprintf("transform: translate(%gpx, 0px); transition: transform ", layout.frame.Width))
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`1s ease;">`)
|
||||||
|
|
||||||
|
viewHTML(layout.pushView, buffer)
|
||||||
|
buffer.WriteString(`</div>`)
|
||||||
|
|
||||||
|
appendToInnerHTML(htmlID, buffer.String(), session)
|
||||||
|
updateCSSProperty(htmlID+"push", "transform", "translate(0px, 0px)", layout.session)
|
||||||
|
|
||||||
|
layout.views = append(layout.views, view)
|
||||||
|
view.setParentID(htmlID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) Pop(animation int, onPopFinished func(View)) bool {
|
||||||
|
count := uint(len(layout.views))
|
||||||
|
if count == 0 || layout.peek >= count {
|
||||||
|
ErrorLog("StackLayout is empty")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.popView = layout.views[layout.peek]
|
||||||
|
layout.RemoveView(layout.peek)
|
||||||
|
|
||||||
|
layout.animationType = animation
|
||||||
|
layout.animation["ruiPop"] = Animation{FinishListener: layout}
|
||||||
|
layout.onPopFinished = onPopFinished
|
||||||
|
|
||||||
|
htmlID := layout.htmlID()
|
||||||
|
session := layout.Session()
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(`<div id="`)
|
||||||
|
buffer.WriteString(htmlID)
|
||||||
|
buffer.WriteString(`pop" class="ruiStackPageLayout" ontransitionend="stackTransitionEndEvent(\'`)
|
||||||
|
buffer.WriteString(htmlID)
|
||||||
|
buffer.WriteString(`\', \'ruiPop\', event)" style="transition: transform 1s ease;">`)
|
||||||
|
viewHTML(layout.popView, buffer)
|
||||||
|
buffer.WriteString(`</div>`)
|
||||||
|
|
||||||
|
appendToInnerHTML(htmlID, buffer.String(), session)
|
||||||
|
|
||||||
|
var value string
|
||||||
|
switch layout.animationType {
|
||||||
|
case TopDownAnimation:
|
||||||
|
value = fmt.Sprintf("translate(0px, -%gpx)", layout.frame.Height)
|
||||||
|
|
||||||
|
case BottomUpAnimation:
|
||||||
|
value = fmt.Sprintf("translate(0px, %gpx)", layout.frame.Height)
|
||||||
|
|
||||||
|
case StartToEndAnimation:
|
||||||
|
value = fmt.Sprintf("translate(-%gpx, 0px)", layout.frame.Width)
|
||||||
|
|
||||||
|
default:
|
||||||
|
value = fmt.Sprintf("translate(%gpx, 0px)", layout.frame.Width)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCSSProperty(htmlID+"pop", "transform", value, layout.session)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout *stackLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
count := len(layout.views)
|
||||||
|
if count > 0 {
|
||||||
|
htmlID := layout.htmlID()
|
||||||
|
peek := int(layout.peek)
|
||||||
|
if peek >= count {
|
||||||
|
peek = count - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, view := range layout.views {
|
||||||
|
buffer.WriteString(`<div id="`)
|
||||||
|
buffer.WriteString(htmlID)
|
||||||
|
buffer.WriteString(`page`)
|
||||||
|
buffer.WriteString(strconv.Itoa(i))
|
||||||
|
buffer.WriteString(`" class="ruiStackPageLayout"`)
|
||||||
|
if i != peek {
|
||||||
|
buffer.WriteString(` style="visibility: hidden;"`)
|
||||||
|
}
|
||||||
|
buffer.WriteString(`>`)
|
||||||
|
viewHTML(view, buffer)
|
||||||
|
buffer.WriteString(`</div>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stringResources = map[string]map[string]string{}
|
||||||
|
|
||||||
|
func scanEmbedStringsDir(fs *embed.FS, dir string) {
|
||||||
|
if files, err := fs.ReadDir(dir); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
path := dir + "/" + name
|
||||||
|
if file.IsDir() {
|
||||||
|
scanEmbedStringsDir(fs, path)
|
||||||
|
} else if strings.ToLower(filepath.Ext(name)) == ".rui" {
|
||||||
|
if data, err := fs.ReadFile(path); err == nil {
|
||||||
|
loadStringResources(string(data))
|
||||||
|
} else {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanStringsDir(path string) {
|
||||||
|
if files, err := ioutil.ReadDir(path); err == nil {
|
||||||
|
for _, file := range files {
|
||||||
|
filename := file.Name()
|
||||||
|
if filename[0] != '.' {
|
||||||
|
newPath := path + `/` + filename
|
||||||
|
if file.IsDir() {
|
||||||
|
scanStringsDir(newPath)
|
||||||
|
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
|
||||||
|
if data, err := ioutil.ReadFile(newPath); err == nil {
|
||||||
|
loadStringResources(string(data))
|
||||||
|
} else {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadStringResources(text string) {
|
||||||
|
data := ParseDataText(text)
|
||||||
|
if data == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parseStrings := func(obj DataObject, lang string) {
|
||||||
|
table, ok := stringResources[lang]
|
||||||
|
if !ok {
|
||||||
|
table = map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < obj.PropertyCount(); i++ {
|
||||||
|
if prop := obj.Property(i); prop != nil && prop.Type() == TextNode {
|
||||||
|
table[prop.Tag()] = prop.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stringResources[lang] = table
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := data.Tag()
|
||||||
|
if tag == "strings" {
|
||||||
|
for i := 0; i < data.PropertyCount(); i++ {
|
||||||
|
if prop := data.Property(i); prop != nil && prop.Type() == ObjectNode {
|
||||||
|
parseStrings(prop.Object(), prop.Tag())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if strings.HasPrefix(tag, "strings:") {
|
||||||
|
if lang := tag[8:]; lang != "" {
|
||||||
|
parseStrings(data, lang)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString returns the text for the language which is defined by "lang" parameter
|
||||||
|
func GetString(tag, lang string) (string, bool) {
|
||||||
|
if table, ok := stringResources[lang]; ok {
|
||||||
|
if text, ok := table[tag]; ok {
|
||||||
|
return text, true
|
||||||
|
}
|
||||||
|
DebugLogF(`There is no "%s" string resource`, tag)
|
||||||
|
}
|
||||||
|
DebugLogF(`There are no "%s" language resources`, lang)
|
||||||
|
return tag, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (session *sessionData) GetString(tag string) (string, bool) {
|
||||||
|
getString := func(tag, lang string) (string, bool) {
|
||||||
|
if table, ok := stringResources[lang]; ok {
|
||||||
|
if text, ok := table[tag]; ok {
|
||||||
|
return text, true
|
||||||
|
}
|
||||||
|
DebugLogF(`There is no "%s" string in "%s" resources`, tag, lang)
|
||||||
|
}
|
||||||
|
return tag, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.language != "" {
|
||||||
|
if text, ok := getString(tag, session.language); ok {
|
||||||
|
return text, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.languages != nil {
|
||||||
|
for _, lang := range session.languages {
|
||||||
|
if lang != session.language {
|
||||||
|
if text, ok := getString(tag, lang); ok {
|
||||||
|
return text, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag, false
|
||||||
|
}
|
|
@ -0,0 +1,331 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
type TableAdapter interface {
|
||||||
|
RowCount() int
|
||||||
|
ColumnCount() int
|
||||||
|
Cell(row, column int) interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableColumnStyle interface {
|
||||||
|
ColumnStyle(column int) Params
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableRowStyle interface {
|
||||||
|
RowStyle(row int) Params
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableCellStyle interface {
|
||||||
|
CellStyle(row, column int) Params
|
||||||
|
}
|
||||||
|
|
||||||
|
type SimpleTableAdapter interface {
|
||||||
|
TableAdapter
|
||||||
|
TableCellStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
type simpleTableAdapter struct {
|
||||||
|
content [][]interface{}
|
||||||
|
columnCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextTableAdapter interface {
|
||||||
|
TableAdapter
|
||||||
|
}
|
||||||
|
|
||||||
|
type textTableAdapter struct {
|
||||||
|
content [][]string
|
||||||
|
columnCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerticalTableJoin struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type HorizontalTableJoin struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSimpleTableAdapter(content [][]interface{}) SimpleTableAdapter {
|
||||||
|
if content == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := new(simpleTableAdapter)
|
||||||
|
adapter.content = content
|
||||||
|
adapter.columnCount = 0
|
||||||
|
for _, row := range content {
|
||||||
|
if row != nil {
|
||||||
|
columnCount := len(row)
|
||||||
|
if adapter.columnCount < columnCount {
|
||||||
|
adapter.columnCount = columnCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *simpleTableAdapter) RowCount() int {
|
||||||
|
if adapter.content != nil {
|
||||||
|
return len(adapter.content)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *simpleTableAdapter) ColumnCount() int {
|
||||||
|
return adapter.columnCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *simpleTableAdapter) Cell(row, column int) interface{} {
|
||||||
|
if adapter.content != nil && row >= 0 && row < len(adapter.content) &&
|
||||||
|
adapter.content[row] != nil && column >= 0 && column < len(adapter.content[row]) {
|
||||||
|
return adapter.content[row][column]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *simpleTableAdapter) CellStyle(row, column int) Params {
|
||||||
|
if adapter.content == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
getColumnSpan := func() int {
|
||||||
|
count := 0
|
||||||
|
for i := column + 1; i < adapter.columnCount; i++ {
|
||||||
|
next := adapter.Cell(row, i)
|
||||||
|
switch next.(type) {
|
||||||
|
case HorizontalTableJoin:
|
||||||
|
count++
|
||||||
|
|
||||||
|
default:
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
getRowSpan := func() int {
|
||||||
|
rowCount := len(adapter.content)
|
||||||
|
count := 0
|
||||||
|
for i := row + 1; i < rowCount; i++ {
|
||||||
|
next := adapter.Cell(i, column)
|
||||||
|
switch next.(type) {
|
||||||
|
case VerticalTableJoin:
|
||||||
|
count++
|
||||||
|
|
||||||
|
default:
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
columnSpan := getColumnSpan()
|
||||||
|
rowSpan := getRowSpan()
|
||||||
|
|
||||||
|
var params Params = nil
|
||||||
|
if rowSpan > 0 {
|
||||||
|
params = Params{RowSpan: rowSpan + 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
if columnSpan > 0 {
|
||||||
|
if params == nil {
|
||||||
|
params = Params{ColumnSpan: columnSpan + 1}
|
||||||
|
} else {
|
||||||
|
params[ColumnSpan] = columnSpan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTextTableAdapter(content [][]string) TextTableAdapter {
|
||||||
|
if content == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter := new(textTableAdapter)
|
||||||
|
adapter.content = content
|
||||||
|
adapter.columnCount = 0
|
||||||
|
for _, row := range content {
|
||||||
|
if row != nil {
|
||||||
|
columnCount := len(row)
|
||||||
|
if adapter.columnCount < columnCount {
|
||||||
|
adapter.columnCount = columnCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *textTableAdapter) RowCount() int {
|
||||||
|
if adapter.content != nil {
|
||||||
|
return len(adapter.content)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *textTableAdapter) ColumnCount() int {
|
||||||
|
return adapter.columnCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (adapter *textTableAdapter) Cell(row, column int) interface{} {
|
||||||
|
if adapter.content != nil && row >= 0 && row < len(adapter.content) &&
|
||||||
|
adapter.content[row] != nil && column >= 0 && column < len(adapter.content[row]) {
|
||||||
|
return adapter.content[row][column]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type simpleTableRowStyle struct {
|
||||||
|
params []Params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *simpleTableRowStyle) RowStyle(row int) Params {
|
||||||
|
if row < len(style.params) {
|
||||||
|
params := style.params[row]
|
||||||
|
if len(params) > 0 {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) setRowStyle(value interface{}) bool {
|
||||||
|
newSimpleTableRowStyle := func(params []Params) TableRowStyle {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := new(simpleTableRowStyle)
|
||||||
|
result.params = params
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case TableRowStyle:
|
||||||
|
table.properties[RowStyle] = value
|
||||||
|
|
||||||
|
case []Params:
|
||||||
|
if style := newSimpleTableRowStyle(value); style != nil {
|
||||||
|
table.properties[RowStyle] = style
|
||||||
|
} else {
|
||||||
|
delete(table.properties, RowStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataNode:
|
||||||
|
if value.Type() == ArrayNode {
|
||||||
|
params := make([]Params, value.ArraySize())
|
||||||
|
for i, element := range value.ArrayElements() {
|
||||||
|
params[i] = Params{}
|
||||||
|
if element.IsObject() {
|
||||||
|
obj := element.Object()
|
||||||
|
for k := 0; k < obj.PropertyCount(); k++ {
|
||||||
|
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
|
||||||
|
params[i][prop.Tag()] = prop.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params[i][Style] = element.Value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if style := newSimpleTableRowStyle(params); style != nil {
|
||||||
|
table.properties[RowStyle] = style
|
||||||
|
} else {
|
||||||
|
delete(table.properties, RowStyle)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) getRowStyle() TableRowStyle {
|
||||||
|
for _, tag := range []string{RowStyle, Content} {
|
||||||
|
if value := table.getRaw(tag); value != nil {
|
||||||
|
if style, ok := value.(TableRowStyle); ok {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type simpleTableColumnStyle struct {
|
||||||
|
params []Params
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *simpleTableColumnStyle) ColumnStyle(row int) Params {
|
||||||
|
if row < len(style.params) {
|
||||||
|
params := style.params[row]
|
||||||
|
if len(params) > 0 {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) setColumnStyle(value interface{}) bool {
|
||||||
|
newSimpleTableColumnStyle := func(params []Params) TableColumnStyle {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := new(simpleTableColumnStyle)
|
||||||
|
result.params = params
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case TableColumnStyle:
|
||||||
|
table.properties[ColumnStyle] = value
|
||||||
|
|
||||||
|
case []Params:
|
||||||
|
if style := newSimpleTableColumnStyle(value); style != nil {
|
||||||
|
table.properties[ColumnStyle] = style
|
||||||
|
} else {
|
||||||
|
delete(table.properties, ColumnStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataNode:
|
||||||
|
if value.Type() == ArrayNode {
|
||||||
|
params := make([]Params, value.ArraySize())
|
||||||
|
for i, element := range value.ArrayElements() {
|
||||||
|
params[i] = Params{}
|
||||||
|
if element.IsObject() {
|
||||||
|
obj := element.Object()
|
||||||
|
for k := 0; k < obj.PropertyCount(); k++ {
|
||||||
|
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
|
||||||
|
params[i][prop.Tag()] = prop.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params[i][Style] = element.Value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if style := newSimpleTableColumnStyle(params); style != nil {
|
||||||
|
table.properties[ColumnStyle] = style
|
||||||
|
} else {
|
||||||
|
delete(table.properties, ColumnStyle)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) getColumnStyle() TableColumnStyle {
|
||||||
|
for _, tag := range []string{ColumnStyle, Content} {
|
||||||
|
if value := table.getRaw(tag); value != nil {
|
||||||
|
if style, ok := value.(TableColumnStyle); ok {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,842 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TableVerticalAlign is the constant for the "table-vertical-align" property tag.
|
||||||
|
// The "table-vertical-align" int property sets the vertical alignment of the content inside a table cell.
|
||||||
|
// Valid values are LeftAlign (0), RightAlign (1), CenterAlign (2), and BaselineAlign (3, 4)
|
||||||
|
TableVerticalAlign = "table-vertical-align"
|
||||||
|
// HeadHeight is the constant for the "head-height" property tag.
|
||||||
|
// The "head-height" int property sets the number of rows in the table header.
|
||||||
|
// The default value is 0 (no header)
|
||||||
|
HeadHeight = "head-height"
|
||||||
|
// HeadStyle is the constant for the "head-style" property tag.
|
||||||
|
// The "head-style" string property sets the header style name
|
||||||
|
HeadStyle = "head-style"
|
||||||
|
// FootHeight is the constant for the "foot-height" property tag.
|
||||||
|
// The "foot-height" int property sets the number of rows in the table footer.
|
||||||
|
// The default value is 0 (no footer)
|
||||||
|
FootHeight = "foot-height"
|
||||||
|
// FootStyle is the constant for the "foot-style" property tag.
|
||||||
|
// The "foot-style" string property sets the footer style name
|
||||||
|
FootStyle = "foot-style"
|
||||||
|
// RowSpan is the constant for the "row-span" property tag.
|
||||||
|
// The "row-span" int property sets the number of table row to span.
|
||||||
|
// Used only when specifying cell parameters in the implementation of TableCellStyle
|
||||||
|
RowSpan = "row-span"
|
||||||
|
// ColumnSpan is the constant for the "column-span" property tag.
|
||||||
|
// The "column-span" int property sets the number of table column to span.
|
||||||
|
// Used only when specifying cell parameters in the implementation of TableCellStyle
|
||||||
|
ColumnSpan = "column-span"
|
||||||
|
// RowStyle is the constant for the "row-style" property tag.
|
||||||
|
// The "row-style" property sets the adapter which specifies styles of each table row.
|
||||||
|
// This property can be assigned or by an implementation of TableRowStyle interface, or by an array of Params.
|
||||||
|
RowStyle = "row-style"
|
||||||
|
// ColumnStyle is the constant for the "column-style" property tag.
|
||||||
|
// The "column-style" property sets the adapter which specifies styles of each table column.
|
||||||
|
// This property can be assigned or by an implementation of TableColumnStyle interface, or by an array of Params.
|
||||||
|
ColumnStyle = "column-style"
|
||||||
|
// CellStyle is the constant for the "cell-style" property tag.
|
||||||
|
// The "cell-style" property sets the adapter which specifies styles of each table cell.
|
||||||
|
// This property can be assigned only by an implementation of TableCellStyle interface.
|
||||||
|
CellStyle = "cell-style"
|
||||||
|
// CellPadding is the constant for the "cell-padding" property tag.
|
||||||
|
// The "cell-padding" Bounds property sets the padding area on all four sides of a table call at once.
|
||||||
|
// An element's padding area is the space between its content and its border.
|
||||||
|
CellPadding = "cell-padding"
|
||||||
|
// CellPaddingLeft is the constant for the "cell-padding-left" property tag.
|
||||||
|
// The "cell-padding-left" SizeUnit property sets the width of the padding area to the left of a cell content.
|
||||||
|
// An element's padding area is the space between its content and its border.
|
||||||
|
CellPaddingLeft = "cell-padding-left"
|
||||||
|
// CellPaddingRight is the constant for the "cell-padding-right" property tag.
|
||||||
|
// The "cell-padding-right" SizeUnit property sets the width of the padding area to the left of a cell content.
|
||||||
|
// An element's padding area is the space between its content and its border.
|
||||||
|
CellPaddingRight = "cell-padding-right"
|
||||||
|
// CellPaddingTop is the constant for the "cell-padding-top" property tag.
|
||||||
|
// The "cell-padding-top" SizeUnit property sets the height of the padding area to the top of a cell content.
|
||||||
|
// An element's padding area is the space between its content and its border.
|
||||||
|
CellPaddingTop = "cell-padding-top"
|
||||||
|
// CellPaddingBottom is the constant for the "cell-padding-bottom" property tag.
|
||||||
|
// The "cell-padding-bottom" SizeUnit property sets the height of the padding area to the bottom of a cell content.
|
||||||
|
CellPaddingBottom = "cell-padding-bottom"
|
||||||
|
// CellBorder is the constant for the "cell-border" property tag.
|
||||||
|
// The "cell-border" property sets a table cell's border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
CellBorder = "cell-border"
|
||||||
|
// CellBorderLeft is the constant for the "cell-border-left" property tag.
|
||||||
|
// The "cell-border-left" property sets a view's left border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
CellBorderLeft = "cell-border-left"
|
||||||
|
// CellBorderRight is the constant for the "cell-border-right" property tag.
|
||||||
|
// The "cell-border-right" property sets a view's right border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
CellBorderRight = "cell-border-right"
|
||||||
|
// CellBorderTop is the constant for the "cell-border-top" property tag.
|
||||||
|
// The "cell-border-top" property sets a view's top border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
CellBorderTop = "cell-border-top"
|
||||||
|
// CellBorderBottom is the constant for the "cell-border-bottom" property tag.
|
||||||
|
// The "cell-border-bottom" property sets a view's bottom border. It sets the values of a border width, style, and color.
|
||||||
|
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
|
||||||
|
CellBorderBottom = "cell-border-bottom"
|
||||||
|
// CellBorderStyle is the constant for the "cell-border-style" property tag.
|
||||||
|
// The "cell-border-style" int property sets the line style for all four sides of a table cell's border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
CellBorderStyle = "cell-border-style"
|
||||||
|
// CellBorderLeftStyle is the constant for the "cell-border-left-style" property tag.
|
||||||
|
// The "cell-border-left-style" int property sets the line style of a table cell's left border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
CellBorderLeftStyle = "cell-border-left-style"
|
||||||
|
// CellBorderRightStyle is the constant for the "cell-border-right-style" property tag.
|
||||||
|
// The "cell-border-right-style" int property sets the line style of a table cell's right border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
CellBorderRightStyle = "cell-border-right-style"
|
||||||
|
// CellBorderTopStyle is the constant for the "cell-border-top-style" property tag.
|
||||||
|
// The "cell-border-top-style" int property sets the line style of a table cell's top border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
CellBorderTopStyle = "cell-border-top-style"
|
||||||
|
// CellBorderBottomStyle is the constant for the "cell-border-bottom-style" property tag.
|
||||||
|
// The "cell-border-bottom-style" int property sets the line style of a table cell's bottom border.
|
||||||
|
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
|
||||||
|
CellBorderBottomStyle = "cell-border-bottom-style"
|
||||||
|
// CellBorderWidth is the constant for the "cell-border-width" property tag.
|
||||||
|
// The "cell-border-width" property sets the line width for all four sides of a table cell's border.
|
||||||
|
CellBorderWidth = "cell-border-width"
|
||||||
|
// CellBorderLeftWidth is the constant for the "cell-border-left-width" property tag.
|
||||||
|
// The "cell-border-left-width" SizeUnit property sets the line width of a table cell's left border.
|
||||||
|
CellBorderLeftWidth = "cell-border-left-width"
|
||||||
|
// CellBorderRightWidth is the constant for the "cell-border-right-width" property tag.
|
||||||
|
// The "cell-border-right-width" SizeUnit property sets the line width of a table cell's right border.
|
||||||
|
CellBorderRightWidth = "cell-border-right-width"
|
||||||
|
// CellBorderTopWidth is the constant for the "cell-border-top-width" property tag.
|
||||||
|
// The "cell-border-top-width" SizeUnit property sets the line width of a table cell's top border.
|
||||||
|
CellBorderTopWidth = "cell-border-top-width"
|
||||||
|
// CellBorderBottomWidth is the constant for the "cell-border-bottom-width" property tag.
|
||||||
|
// The "cell-border-bottom-width" SizeUnit property sets the line width of a table cell's bottom border.
|
||||||
|
CellBorderBottomWidth = "cell-border-bottom-width"
|
||||||
|
// CellBorderColor is the constant for the "cell-border-color" property tag.
|
||||||
|
// The "cell-border-color" property sets the line color for all four sides of a table cell's border.
|
||||||
|
CellBorderColor = "cell-border-color"
|
||||||
|
// CellBorderLeftColor is the constant for the "cell-border-left-color" property tag.
|
||||||
|
// The "cell-border-left-color" property sets the line color of a table cell's left border.
|
||||||
|
CellBorderLeftColor = "cell-border-left-color"
|
||||||
|
// CellBorderRightColor is the constant for the "cell-border-right-color" property tag.
|
||||||
|
// The "cell-border-right-color" property sets the line color of a table cell's right border.
|
||||||
|
CellBorderRightColor = "cell-border-right-color"
|
||||||
|
// CellBorderTopColor is the constant for the "cell-border-top-color" property tag.
|
||||||
|
// The "cell-border-top-color" property sets the line color of a table cell's top border.
|
||||||
|
CellBorderTopColor = "cell-border-top-color"
|
||||||
|
// CellBorderBottomColor is the constant for the "cell-border-bottom-color" property tag.
|
||||||
|
// The "cell-border-bottom-color" property sets the line color of a table cell's bottom border.
|
||||||
|
CellBorderBottomColor = "cell-border-bottom-color"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TableView - text View
|
||||||
|
type TableView interface {
|
||||||
|
View
|
||||||
|
ReloadTableData()
|
||||||
|
}
|
||||||
|
|
||||||
|
type tableViewData struct {
|
||||||
|
viewData
|
||||||
|
}
|
||||||
|
|
||||||
|
type tableCellView struct {
|
||||||
|
viewData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTableView create new TableView object and return it
|
||||||
|
func NewTableView(session Session, params Params) TableView {
|
||||||
|
view := new(tableViewData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTableView(session Session) View {
|
||||||
|
return NewTableView(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of TableView by default values
|
||||||
|
func (table *tableViewData) Init(session Session) {
|
||||||
|
table.viewData.Init(session)
|
||||||
|
table.tag = "TableView"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) Get(tag string) interface{} {
|
||||||
|
return table.get(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) Remove(tag string) {
|
||||||
|
table.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
|
||||||
|
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft,
|
||||||
|
"top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding":
|
||||||
|
table.removeBoundsSide(CellPadding, tag)
|
||||||
|
|
||||||
|
case Gap, CellBorder, CellPadding, RowStyle, ColumnStyle, CellStyle,
|
||||||
|
HeadHeight, HeadStyle, FootHeight, FootStyle:
|
||||||
|
delete(table.properties, tag)
|
||||||
|
|
||||||
|
default:
|
||||||
|
table.viewData.remove(tag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
table.propertyChanged(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) Set(tag string, value interface{}) bool {
|
||||||
|
return table.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
table.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Content:
|
||||||
|
switch val := value.(type) {
|
||||||
|
case TableAdapter:
|
||||||
|
table.properties[Content] = value
|
||||||
|
|
||||||
|
case [][]interface{}:
|
||||||
|
table.properties[Content] = NewSimpleTableAdapter(val)
|
||||||
|
|
||||||
|
case [][]string:
|
||||||
|
table.properties[Content] = NewTextTableAdapter(val)
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case CellStyle:
|
||||||
|
if style, ok := value.(TableCellStyle); ok {
|
||||||
|
table.properties[tag] = style
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case RowStyle:
|
||||||
|
if !table.setRowStyle(value) {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case ColumnStyle:
|
||||||
|
if !table.setColumnStyle(value) {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case HeadHeight, FootHeight:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if isConstantName(value) {
|
||||||
|
table.properties[tag] = value
|
||||||
|
} else if n, err := strconv.Atoi(value); err == nil {
|
||||||
|
table.properties[tag] = n
|
||||||
|
} else {
|
||||||
|
ErrorLog(err.Error())
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
table.properties[tag] = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case HeadStyle, FootStyle:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
table.properties[tag] = value
|
||||||
|
|
||||||
|
case Params:
|
||||||
|
if len(value) > 0 {
|
||||||
|
table.properties[tag] = value
|
||||||
|
} else {
|
||||||
|
delete(table.properties, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataNode:
|
||||||
|
switch value.Type() {
|
||||||
|
case ObjectNode:
|
||||||
|
obj := value.Object()
|
||||||
|
params := Params{}
|
||||||
|
for k := 0; k < obj.PropertyCount(); k++ {
|
||||||
|
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
|
||||||
|
params[prop.Tag()] = prop.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(params) > 0 {
|
||||||
|
table.properties[tag] = params
|
||||||
|
} else {
|
||||||
|
delete(table.properties, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
case TextNode:
|
||||||
|
table.properties[tag] = value.Text()
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case CellPadding:
|
||||||
|
if !table.setBounds(tag, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft,
|
||||||
|
"top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding":
|
||||||
|
if !table.setBoundsSide(CellPadding, tag, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case Gap:
|
||||||
|
if !table.setSizeProperty(Gap, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
case CellBorder, CellBorderStyle, CellBorderColor, CellBorderWidth,
|
||||||
|
CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth,
|
||||||
|
CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth,
|
||||||
|
CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth,
|
||||||
|
CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth:
|
||||||
|
if !table.viewData.set(tag, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return table.viewData.set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.propertyChanged(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) propertyChanged(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case Content, RowStyle, ColumnStyle, CellStyle, CellPadding, CellBorder,
|
||||||
|
HeadHeight, HeadStyle, FootHeight, FootStyle,
|
||||||
|
CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft,
|
||||||
|
"top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding":
|
||||||
|
table.ReloadTableData()
|
||||||
|
|
||||||
|
case Gap:
|
||||||
|
htmlID := table.htmlID()
|
||||||
|
session := table.Session()
|
||||||
|
gap, ok := sizeProperty(table, Gap, session)
|
||||||
|
if !ok || gap.Type == Auto || gap.Value <= 0 {
|
||||||
|
updateCSSProperty(htmlID, "border-spacing", "0", session)
|
||||||
|
updateCSSProperty(htmlID, "border-collapse", "collapse", session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, "border-spacing", gap.cssString("0"), session)
|
||||||
|
updateCSSProperty(htmlID, "border-collapse", "separate", session)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) htmlTag() string {
|
||||||
|
return "table"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
content := table.getRaw(Content)
|
||||||
|
if content == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter, ok := content.(TableAdapter)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rowCount := adapter.RowCount()
|
||||||
|
columnCount := adapter.ColumnCount()
|
||||||
|
if rowCount == 0 || columnCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rowStyle := table.getRowStyle()
|
||||||
|
|
||||||
|
var cellStyle1 TableCellStyle = nil
|
||||||
|
if style, ok := content.(TableCellStyle); ok {
|
||||||
|
cellStyle1 = style
|
||||||
|
}
|
||||||
|
|
||||||
|
var cellStyle2 TableCellStyle = nil
|
||||||
|
if value := table.getRaw(CellStyle); value != nil {
|
||||||
|
if style, ok := value.(TableCellStyle); ok {
|
||||||
|
cellStyle2 = style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
session := table.Session()
|
||||||
|
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
session.setIgnoreViewUpdates(true)
|
||||||
|
defer session.setIgnoreViewUpdates(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cssBuilder viewCSSBuilder
|
||||||
|
cssBuilder.buffer = allocStringBuilder()
|
||||||
|
defer freeStringBuilder(cssBuilder.buffer)
|
||||||
|
|
||||||
|
var view tableCellView
|
||||||
|
view.Init(session)
|
||||||
|
|
||||||
|
ignorCells := []struct{ row, column int }{}
|
||||||
|
|
||||||
|
tableCSS := func(startRow, endRow int, cellTag string, cellBorder BorderProperty, cellPadding BoundsProperty) {
|
||||||
|
for row := startRow; row < endRow; row++ {
|
||||||
|
|
||||||
|
cssBuilder.buffer.Reset()
|
||||||
|
if rowStyle != nil {
|
||||||
|
if styles := rowStyle.RowStyle(row); styles != nil {
|
||||||
|
view.Clear()
|
||||||
|
for tag, value := range styles {
|
||||||
|
view.Set(tag, value)
|
||||||
|
}
|
||||||
|
view.cssStyle(&view, &cssBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cssBuilder.buffer.Len() > 0 {
|
||||||
|
buffer.WriteString(`<tr style="`)
|
||||||
|
buffer.WriteString(cssBuilder.buffer.String())
|
||||||
|
buffer.WriteString(`">`)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString("<tr>")
|
||||||
|
}
|
||||||
|
|
||||||
|
for column := 0; column < columnCount; column++ {
|
||||||
|
ignore := false
|
||||||
|
for _, cell := range ignorCells {
|
||||||
|
if cell.row == row && cell.column == column {
|
||||||
|
ignore = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ignore {
|
||||||
|
rowSpan := 0
|
||||||
|
columnSpan := 0
|
||||||
|
|
||||||
|
cssBuilder.buffer.Reset()
|
||||||
|
view.Clear()
|
||||||
|
|
||||||
|
if cellBorder != nil {
|
||||||
|
view.set(Border, cellBorder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cellPadding != nil {
|
||||||
|
view.set(Padding, cellPadding)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendFrom := func(cellStyle TableCellStyle) {
|
||||||
|
if cellStyle != nil {
|
||||||
|
if styles := cellStyle.CellStyle(row, column); styles != nil {
|
||||||
|
for tag, value := range styles {
|
||||||
|
valueToInt := func() int {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case int:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if value, ok = session.resolveConstants(value); ok {
|
||||||
|
if n, err := strconv.Atoi(value); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag = strings.ToLower(tag); tag {
|
||||||
|
case RowSpan:
|
||||||
|
rowSpan = valueToInt()
|
||||||
|
|
||||||
|
case ColumnSpan:
|
||||||
|
columnSpan = valueToInt()
|
||||||
|
|
||||||
|
default:
|
||||||
|
view.set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendFrom(cellStyle1)
|
||||||
|
appendFrom(cellStyle2)
|
||||||
|
|
||||||
|
if len(view.properties) > 0 {
|
||||||
|
view.cssStyle(&view, &cssBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteRune('<')
|
||||||
|
buffer.WriteString(cellTag)
|
||||||
|
|
||||||
|
if columnSpan > 1 {
|
||||||
|
buffer.WriteString(` colspan="`)
|
||||||
|
buffer.WriteString(strconv.Itoa(columnSpan))
|
||||||
|
buffer.WriteRune('"')
|
||||||
|
for c := column + 1; c < column+columnSpan; c++ {
|
||||||
|
ignorCells = append(ignorCells, struct {
|
||||||
|
row int
|
||||||
|
column int
|
||||||
|
}{row: row, column: c})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowSpan > 1 {
|
||||||
|
buffer.WriteString(` rowspan="`)
|
||||||
|
buffer.WriteString(strconv.Itoa(rowSpan))
|
||||||
|
buffer.WriteRune('"')
|
||||||
|
if columnSpan < 1 {
|
||||||
|
columnSpan = 1
|
||||||
|
}
|
||||||
|
for r := row + 1; r < row+rowSpan; r++ {
|
||||||
|
for c := column; c < column+columnSpan; c++ {
|
||||||
|
ignorCells = append(ignorCells, struct {
|
||||||
|
row int
|
||||||
|
column int
|
||||||
|
}{row: r, column: c})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cssBuilder.buffer.Len() > 0 {
|
||||||
|
buffer.WriteString(` style="`)
|
||||||
|
buffer.WriteString(cssBuilder.buffer.String())
|
||||||
|
buffer.WriteRune('"')
|
||||||
|
}
|
||||||
|
buffer.WriteRune('>')
|
||||||
|
|
||||||
|
switch value := adapter.Cell(row, column).(type) {
|
||||||
|
case string:
|
||||||
|
buffer.WriteString(value)
|
||||||
|
|
||||||
|
case View:
|
||||||
|
viewHTML(value, buffer)
|
||||||
|
|
||||||
|
case Color:
|
||||||
|
buffer.WriteString(`<div style="display: inline; height: 1em; background-color: `)
|
||||||
|
buffer.WriteString(value.cssString())
|
||||||
|
buffer.WriteString(`"> </div> `)
|
||||||
|
buffer.WriteString(value.String())
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
buffer.WriteString(value.String())
|
||||||
|
|
||||||
|
case rune:
|
||||||
|
buffer.WriteRune(value)
|
||||||
|
|
||||||
|
case float32:
|
||||||
|
buffer.WriteString(fmt.Sprintf("%g", float64(value)))
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
buffer.WriteString(fmt.Sprintf("%g", value))
|
||||||
|
|
||||||
|
case bool:
|
||||||
|
if value {
|
||||||
|
buffer.WriteString(session.checkboxOnImage())
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(session.checkboxOffImage())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
buffer.WriteString(fmt.Sprintf("%d", n))
|
||||||
|
} else {
|
||||||
|
buffer.WriteString("<Unsupported value>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`</`)
|
||||||
|
buffer.WriteString(cellTag)
|
||||||
|
buffer.WriteRune('>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString("</tr>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if columnStyle := table.getColumnStyle(); columnStyle != nil {
|
||||||
|
buffer.WriteString("<colgroup>")
|
||||||
|
for column := 0; column < columnCount; column++ {
|
||||||
|
cssBuilder.buffer.Reset()
|
||||||
|
if styles := columnStyle.ColumnStyle(column); styles != nil {
|
||||||
|
view.Clear()
|
||||||
|
for tag, value := range styles {
|
||||||
|
view.Set(tag, value)
|
||||||
|
}
|
||||||
|
view.cssStyle(&view, &cssBuilder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cssBuilder.buffer.Len() > 0 {
|
||||||
|
buffer.WriteString(`<col style="`)
|
||||||
|
buffer.WriteString(cssBuilder.buffer.String())
|
||||||
|
buffer.WriteString(`">`)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString("<col>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.WriteString("</colgroup>")
|
||||||
|
}
|
||||||
|
|
||||||
|
headHeight, _ := intProperty(table, HeadHeight, table.Session(), 0)
|
||||||
|
footHeight, _ := intProperty(table, FootHeight, table.Session(), 0)
|
||||||
|
cellBorder := table.getCellBorder()
|
||||||
|
cellPadding := table.boundsProperty(CellPadding)
|
||||||
|
if cellPadding == nil {
|
||||||
|
if style, ok := stringProperty(table, Style, table.Session()); ok {
|
||||||
|
if style, ok := table.Session().resolveConstants(style); ok {
|
||||||
|
cellPadding = table.cellPaddingFromStyle(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headFootStart := func(htmlTag, styleTag string) (BorderProperty, BoundsProperty) {
|
||||||
|
buffer.WriteRune('<')
|
||||||
|
buffer.WriteString(htmlTag)
|
||||||
|
if value := table.getRaw(styleTag); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if style, ok := session.resolveConstants(value); ok {
|
||||||
|
buffer.WriteString(` class="`)
|
||||||
|
buffer.WriteString(style)
|
||||||
|
buffer.WriteString(`">`)
|
||||||
|
return table.cellBorderFromStyle(style), table.cellPaddingFromStyle(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Params:
|
||||||
|
cssBuilder.buffer.Reset()
|
||||||
|
view.Clear()
|
||||||
|
for tag, val := range value {
|
||||||
|
view.Set(tag, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
var border BorderProperty = nil
|
||||||
|
if value := view.Get(CellBorder); value != nil {
|
||||||
|
border = value.(BorderProperty)
|
||||||
|
}
|
||||||
|
var padding BoundsProperty = nil
|
||||||
|
if value := view.Get(CellPadding); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case SizeUnit:
|
||||||
|
padding = NewBoundsProperty(Params{
|
||||||
|
Top: value,
|
||||||
|
Right: value,
|
||||||
|
Bottom: value,
|
||||||
|
Left: value,
|
||||||
|
})
|
||||||
|
|
||||||
|
case BoundsProperty:
|
||||||
|
padding = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.cssStyle(&view, &cssBuilder)
|
||||||
|
if cssBuilder.buffer.Len() > 0 {
|
||||||
|
buffer.WriteString(` style="`)
|
||||||
|
buffer.WriteString(cssBuilder.buffer.String())
|
||||||
|
buffer.WriteString(`"`)
|
||||||
|
}
|
||||||
|
buffer.WriteRune('>')
|
||||||
|
return border, padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.WriteRune('>')
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if headHeight > 0 {
|
||||||
|
headCellBorder := cellBorder
|
||||||
|
headCellPadding := cellPadding
|
||||||
|
|
||||||
|
if headHeight > rowCount {
|
||||||
|
headHeight = rowCount
|
||||||
|
}
|
||||||
|
|
||||||
|
border, padding := headFootStart("thead", HeadStyle)
|
||||||
|
if border != nil {
|
||||||
|
headCellBorder = border
|
||||||
|
}
|
||||||
|
if padding != nil {
|
||||||
|
headCellPadding = padding
|
||||||
|
}
|
||||||
|
tableCSS(0, headHeight, "th", headCellBorder, headCellPadding)
|
||||||
|
buffer.WriteString("</thead>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if footHeight > rowCount-headHeight {
|
||||||
|
footHeight = rowCount - headHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
if rowCount > footHeight+headHeight {
|
||||||
|
buffer.WriteString("<tbody>")
|
||||||
|
tableCSS(headHeight, rowCount-footHeight, "td", cellBorder, cellPadding)
|
||||||
|
buffer.WriteString("</tbody>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if footHeight > 0 {
|
||||||
|
footCellBorder := cellBorder
|
||||||
|
footCellPadding := cellPadding
|
||||||
|
|
||||||
|
border, padding := headFootStart("tfoot", FootStyle)
|
||||||
|
if border != nil {
|
||||||
|
footCellBorder = border
|
||||||
|
}
|
||||||
|
if padding != nil {
|
||||||
|
footCellPadding = padding
|
||||||
|
}
|
||||||
|
tableCSS(rowCount-footHeight, rowCount, "td", footCellBorder, footCellPadding)
|
||||||
|
buffer.WriteString("</tfoot>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) cellPaddingFromStyle(style string) BoundsProperty {
|
||||||
|
session := table.Session()
|
||||||
|
var result BoundsProperty = nil
|
||||||
|
|
||||||
|
if node := session.stylePropertyNode(style, CellPadding); node != nil && node.Type() == ObjectNode {
|
||||||
|
for _, tag := range []string{Left, Right, Top, Bottom} {
|
||||||
|
if node := node.Object().PropertyWithTag(tag); node != nil && node.Type() == TextNode {
|
||||||
|
if result == nil {
|
||||||
|
result = NewBoundsProperty(nil)
|
||||||
|
}
|
||||||
|
result.Set(tag, node.Text())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range []string{CellPaddingLeft, CellPaddingRight, CellPaddingTop, CellPaddingBottom} {
|
||||||
|
if value, ok := session.styleProperty(style, CellPadding); ok {
|
||||||
|
if result == nil {
|
||||||
|
result = NewBoundsProperty(nil)
|
||||||
|
}
|
||||||
|
result.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) cellBorderFromStyle(style string) BorderProperty {
|
||||||
|
|
||||||
|
border := new(borderProperty)
|
||||||
|
border.properties = map[string]interface{}{}
|
||||||
|
|
||||||
|
session := table.Session()
|
||||||
|
if node := session.stylePropertyNode(style, CellBorder); node != nil && node.Type() == ObjectNode {
|
||||||
|
border.setBorderObject(node.Object())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range []string{
|
||||||
|
CellBorderLeft,
|
||||||
|
CellBorderRight,
|
||||||
|
CellBorderTop,
|
||||||
|
CellBorderBottom,
|
||||||
|
CellBorderStyle,
|
||||||
|
CellBorderLeftStyle,
|
||||||
|
CellBorderRightStyle,
|
||||||
|
CellBorderTopStyle,
|
||||||
|
CellBorderBottomStyle,
|
||||||
|
CellBorderWidth,
|
||||||
|
CellBorderLeftWidth,
|
||||||
|
CellBorderRightWidth,
|
||||||
|
CellBorderTopWidth,
|
||||||
|
CellBorderBottomWidth,
|
||||||
|
CellBorderColor,
|
||||||
|
CellBorderLeftColor,
|
||||||
|
CellBorderRightColor,
|
||||||
|
CellBorderTopColor,
|
||||||
|
CellBorderBottomColor,
|
||||||
|
} {
|
||||||
|
if value, ok := session.styleProperty(style, tag); ok {
|
||||||
|
border.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(border.properties) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return border
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) getCellBorder() BorderProperty {
|
||||||
|
if value := table.getRaw(CellBorder); value != nil {
|
||||||
|
if border, ok := value.(BorderProperty); ok {
|
||||||
|
return border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if style, ok := stringProperty(table, Style, table.Session()); ok {
|
||||||
|
if style, ok := table.Session().resolveConstants(style); ok {
|
||||||
|
return table.cellBorderFromStyle(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) cssStyle(self View, builder cssBuilder) {
|
||||||
|
table.viewData.cssViewStyle(builder, table.Session(), self)
|
||||||
|
|
||||||
|
gap, ok := sizeProperty(table, Gap, table.Session())
|
||||||
|
if !ok || gap.Type == Auto || gap.Value <= 0 {
|
||||||
|
builder.add("border-spacing", "0")
|
||||||
|
builder.add("border-collapse", "collapse")
|
||||||
|
} else {
|
||||||
|
builder.add("border-spacing", gap.cssString("0"))
|
||||||
|
builder.add("border-collapse", "separate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *tableViewData) ReloadTableData() {
|
||||||
|
updateInnerHTML(table.htmlID(), table.Session())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *tableCellView) Set(tag string, value interface{}) bool {
|
||||||
|
return cell.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *tableCellView) set(tag string, value interface{}) bool {
|
||||||
|
switch tag {
|
||||||
|
case VerticalAlign:
|
||||||
|
tag = TableVerticalAlign
|
||||||
|
}
|
||||||
|
return cell.viewData.set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
|
||||||
|
session := cell.Session()
|
||||||
|
cell.viewData.cssViewStyle(builder, session, self)
|
||||||
|
|
||||||
|
if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok {
|
||||||
|
builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,490 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HiddenTabs - tabs of TabsLayout are hidden
|
||||||
|
HiddenTabs = 0
|
||||||
|
// TopTabs - tabs of TabsLayout are on the top
|
||||||
|
TopTabs = 1
|
||||||
|
// BottomTabs - tabs of TabsLayout are on the bottom
|
||||||
|
BottomTabs = 2
|
||||||
|
// LeftTabs - tabs of TabsLayout are on the left
|
||||||
|
LeftTabs = 3
|
||||||
|
// RightTabs - tabs of TabsLayout are on the right
|
||||||
|
RightTabs = 4
|
||||||
|
// LeftListTabs - tabs of TabsLayout are on the left
|
||||||
|
LeftListTabs = 5
|
||||||
|
// RightListTabs - tabs of TabsLayout are on the right
|
||||||
|
RightListTabs = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// TabsLayoutCurrentChangedListener - listener of the current tab changing
|
||||||
|
type TabsLayoutCurrentChangedListener interface {
|
||||||
|
OnTabsLayoutCurrentChanged(tabsLayout TabsLayout, newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tabsLayoutCurrentChangedListenerFunc struct {
|
||||||
|
listenerFunc func(tabsLayout TabsLayout, newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener *tabsLayoutCurrentChangedListenerFunc) OnTabsLayoutCurrentChanged(tabsLayout TabsLayout,
|
||||||
|
newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View) {
|
||||||
|
if listener.listenerFunc != nil {
|
||||||
|
listener.listenerFunc(tabsLayout, newCurrent, newCurrentView, oldCurrent, oldCurrentView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TabsLayout - multi-tab container of View
|
||||||
|
type TabsLayout interface {
|
||||||
|
ViewsContainer
|
||||||
|
/*
|
||||||
|
// Current return the index of active tab
|
||||||
|
currentItem() int
|
||||||
|
// SetCurrent set the index of active tab
|
||||||
|
SetCurrent(current int)
|
||||||
|
// TabsLocation return the location of tabs. It returns one of the following values: HiddenTabs (0),
|
||||||
|
// TopTabs (1), BottomTabs (2), LeftTabs (3), RightTabs (4), LeftListTabs (5), RightListTabs (6)
|
||||||
|
tabsLocation() int
|
||||||
|
// TabsLocation set the location of tabs. Valid values: HiddenTabs (0), TopTabs (1),
|
||||||
|
// BottomTabs (2), LeftTabs (3), RightTabs (4), LeftListTabs (5), RightListTabs (6)
|
||||||
|
SetTabsLocation(location int)
|
||||||
|
// TabStyle() return styles of tab in the passive and the active state
|
||||||
|
TabStyle() (string, string)
|
||||||
|
SetTabStyle(tabStyle string, activeTabStyle string)
|
||||||
|
*/
|
||||||
|
// SetCurrentTabChangedListener add the listener of the current tab changing
|
||||||
|
SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener)
|
||||||
|
// SetCurrentTabChangedListener add the listener function of the current tab changing
|
||||||
|
SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout,
|
||||||
|
newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View))
|
||||||
|
}
|
||||||
|
|
||||||
|
type tabsLayoutData struct {
|
||||||
|
viewsContainerData
|
||||||
|
//currentTab, tabsLocation int
|
||||||
|
//tabStyle, activeTabStyle string
|
||||||
|
tabListener TabsLayoutCurrentChangedListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTabsLayout create new TabsLayout object and return it
|
||||||
|
func NewTabsLayout(session Session) TabsLayout {
|
||||||
|
view := new(tabsLayoutData)
|
||||||
|
view.Init(session)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTabsLayout(session Session) View {
|
||||||
|
return NewTabsLayout(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of ViewsContainer by default values
|
||||||
|
func (tabsLayout *tabsLayoutData) Init(session Session) {
|
||||||
|
tabsLayout.viewsContainerData.Init(session)
|
||||||
|
tabsLayout.tag = "TabsLayout"
|
||||||
|
tabsLayout.systemClass = "ruiTabsLayout"
|
||||||
|
tabsLayout.tabListener = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) currentItem() int {
|
||||||
|
result, _ := intProperty(tabsLayout, Current, tabsLayout.session, 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) Set(tag string, value interface{}) bool {
|
||||||
|
switch tag {
|
||||||
|
case Current:
|
||||||
|
oldCurrent := tabsLayout.currentItem()
|
||||||
|
if !tabsLayout.setIntProperty(Current, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tabsLayout.session.ignoreViewUpdates() {
|
||||||
|
current := tabsLayout.currentItem()
|
||||||
|
if oldCurrent != current {
|
||||||
|
tabsLayout.session.runScript(fmt.Sprintf("activateTab(%v, %d);", tabsLayout.htmlID(), current))
|
||||||
|
if tabsLayout.tabListener != nil {
|
||||||
|
oldView := tabsLayout.views[oldCurrent]
|
||||||
|
view := tabsLayout.views[current]
|
||||||
|
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, current, view, oldCurrent, oldView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case Tabs:
|
||||||
|
if !tabsLayout.setEnumProperty(Tabs, value, enumProperties[Tabs].values) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !tabsLayout.session.ignoreViewUpdates() {
|
||||||
|
htmlID := tabsLayout.htmlID()
|
||||||
|
updateCSSStyle(htmlID, tabsLayout.session)
|
||||||
|
updateInnerHTML(htmlID, tabsLayout.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case TabStyle, CurrentTabStyle:
|
||||||
|
if value == nil {
|
||||||
|
delete(tabsLayout.properties, tag)
|
||||||
|
} else if text, ok := value.(string); ok {
|
||||||
|
if text == "" {
|
||||||
|
delete(tabsLayout.properties, tag)
|
||||||
|
} else {
|
||||||
|
tabsLayout.properties[tag] = text
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tabsLayout.session.ignoreViewUpdates() {
|
||||||
|
htmlID := tabsLayout.htmlID()
|
||||||
|
updateProperty(htmlID, "data-tabStyle", tabsLayout.inactiveTabStyle(), tabsLayout.session)
|
||||||
|
updateProperty(htmlID, "data-activeTabStyle", tabsLayout.activeTabStyle(), tabsLayout.session)
|
||||||
|
updateInnerHTML(htmlID, tabsLayout.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return tabsLayout.viewsContainerData.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) tabsLocation() int {
|
||||||
|
tabs, _ := enumProperty(tabsLayout, Tabs, tabsLayout.session, 0)
|
||||||
|
return tabs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) inactiveTabStyle() string {
|
||||||
|
if style, ok := stringProperty(tabsLayout, TabStyle, tabsLayout.session); ok {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
switch tabsLayout.tabsLocation() {
|
||||||
|
case LeftTabs, RightTabs:
|
||||||
|
return "ruiInactiveVerticalTab"
|
||||||
|
}
|
||||||
|
return "ruiInactiveTab"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) activeTabStyle() string {
|
||||||
|
if style, ok := stringProperty(tabsLayout, CurrentTabStyle, tabsLayout.session); ok {
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
switch tabsLayout.tabsLocation() {
|
||||||
|
case LeftTabs, RightTabs:
|
||||||
|
return "ruiActiveVerticalTab"
|
||||||
|
}
|
||||||
|
return "ruiActiveTab"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) TabStyle() (string, string) {
|
||||||
|
return tabsLayout.inactiveTabStyle(), tabsLayout.activeTabStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener) {
|
||||||
|
tabsLayout.tabListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// SetCurrentTabChangedListener add the listener of the current tab changing
|
||||||
|
func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener) {
|
||||||
|
tabsLayout.tabListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCurrentTabChangedListener add the listener function of the current tab changing
|
||||||
|
func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout,
|
||||||
|
newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)) {
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout,
|
||||||
|
newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)) {
|
||||||
|
listener := new(tabsLayoutCurrentChangedListenerFunc)
|
||||||
|
listener.listenerFunc = listenerFunc
|
||||||
|
tabsLayout.SetCurrentTabChangedListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append appends view to the end of view list
|
||||||
|
func (tabsLayout *tabsLayoutData) Append(view View) {
|
||||||
|
if tabsLayout.views == nil {
|
||||||
|
tabsLayout.views = []View{}
|
||||||
|
}
|
||||||
|
tabsLayout.viewsContainerData.Append(view)
|
||||||
|
if len(tabsLayout.views) == 1 {
|
||||||
|
tabsLayout.setIntProperty(Current, 0)
|
||||||
|
if tabsLayout.tabListener != nil {
|
||||||
|
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, 0, tabsLayout.views[0], -1, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert inserts view to the "index" position in view list
|
||||||
|
func (tabsLayout *tabsLayoutData) Insert(view View, index uint) {
|
||||||
|
if tabsLayout.views == nil {
|
||||||
|
tabsLayout.views = []View{}
|
||||||
|
}
|
||||||
|
tabsLayout.viewsContainerData.Insert(view, index)
|
||||||
|
current := tabsLayout.currentItem()
|
||||||
|
if current >= int(index) {
|
||||||
|
tabsLayout.Set(Current, current+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes view from list and return it
|
||||||
|
func (tabsLayout *tabsLayoutData) RemoveView(index uint) View {
|
||||||
|
if tabsLayout.views == nil {
|
||||||
|
tabsLayout.views = []View{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i := int(index)
|
||||||
|
count := len(tabsLayout.views)
|
||||||
|
if i >= count {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 1 {
|
||||||
|
view := tabsLayout.views[0]
|
||||||
|
tabsLayout.views = []View{}
|
||||||
|
if tabsLayout.tabListener != nil {
|
||||||
|
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, 0, nil, 0, view)
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
current := tabsLayout.currentItem()
|
||||||
|
removeCurrent := (i == current)
|
||||||
|
if i < current || (removeCurrent && i == count-1) {
|
||||||
|
tabsLayout.properties[Current] = current - 1
|
||||||
|
if tabsLayout.tabListener != nil {
|
||||||
|
currentView := tabsLayout.views[current-1]
|
||||||
|
oldCurrentView := tabsLayout.views[current]
|
||||||
|
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, current-1, currentView, current, oldCurrentView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabsLayout.viewsContainerData.RemoveView(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
tabsLayout.viewsContainerData.htmlProperties(self, buffer)
|
||||||
|
buffer.WriteString(` data-inactiveTabStyle="`)
|
||||||
|
buffer.WriteString(tabsLayout.inactiveTabStyle())
|
||||||
|
buffer.WriteString(`" data-activeTabStyle="`)
|
||||||
|
buffer.WriteString(tabsLayout.activeTabStyle())
|
||||||
|
buffer.WriteString(`" data-current="`)
|
||||||
|
buffer.WriteString(tabsLayout.htmlID())
|
||||||
|
buffer.WriteRune('-')
|
||||||
|
buffer.WriteString(strconv.Itoa(tabsLayout.currentItem()))
|
||||||
|
buffer.WriteRune('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) cssStyle(self View, builder cssBuilder) {
|
||||||
|
tabsLayout.viewsContainerData.cssStyle(self, builder)
|
||||||
|
switch tabsLayout.tabsLocation() {
|
||||||
|
case TopTabs:
|
||||||
|
builder.add(`grid-template-rows`, `auto 1fr`)
|
||||||
|
|
||||||
|
case BottomTabs:
|
||||||
|
builder.add(`grid-template-rows`, `1fr auto`)
|
||||||
|
|
||||||
|
case LeftTabs, LeftListTabs:
|
||||||
|
builder.add(`grid-template-columns`, `auto 1fr`)
|
||||||
|
|
||||||
|
case RightTabs, RightListTabs:
|
||||||
|
builder.add(`grid-template-columns`, `1fr auto`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
if tabsLayout.views == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//viewCount := len(tabsLayout.views)
|
||||||
|
current := tabsLayout.currentItem()
|
||||||
|
location := tabsLayout.tabsLocation()
|
||||||
|
tabsLayoutID := tabsLayout.htmlID()
|
||||||
|
|
||||||
|
if location != HiddenTabs {
|
||||||
|
tabsHeight, _ := sizeConstant(tabsLayout.session, "ruiTabHeight")
|
||||||
|
tabsSpace, _ := sizeConstant(tabsLayout.session, "ruiTabSpace")
|
||||||
|
rowLayout := false
|
||||||
|
buffer.WriteString(`<div style="display: flex;`)
|
||||||
|
|
||||||
|
switch location {
|
||||||
|
case LeftTabs, LeftListTabs, TopTabs:
|
||||||
|
buffer.WriteString(` grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;`)
|
||||||
|
|
||||||
|
case RightTabs, RightListTabs:
|
||||||
|
buffer.WriteString(` grid-row-start: 1; grid-row-end: 2; grid-column-start: 2; grid-column-end: 3;`)
|
||||||
|
|
||||||
|
case BottomTabs:
|
||||||
|
buffer.WriteString(` grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(` flex-flow: `)
|
||||||
|
switch location {
|
||||||
|
case LeftTabs, LeftListTabs, RightTabs, RightListTabs:
|
||||||
|
buffer.WriteString(`column nowrap; justify-content: flex-start; align-items: stretch;`)
|
||||||
|
|
||||||
|
default:
|
||||||
|
buffer.WriteString(`row nowrap; justify-content: flex-start; align-items: stretch;`)
|
||||||
|
if tabsHeight.Type != Auto {
|
||||||
|
buffer.WriteString(` height: `)
|
||||||
|
buffer.WriteString(tabsHeight.cssString(""))
|
||||||
|
buffer.WriteByte(';')
|
||||||
|
}
|
||||||
|
rowLayout = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var tabsPadding Bounds
|
||||||
|
if value, ok := tabsLayout.session.Constant("ruiTabPadding"); ok {
|
||||||
|
if tabsPadding.parse(value, tabsLayout.session) {
|
||||||
|
if !tabsPadding.allFieldsAuto() {
|
||||||
|
buffer.WriteByte(' ')
|
||||||
|
buffer.WriteString(Padding)
|
||||||
|
buffer.WriteString(`: `)
|
||||||
|
tabsPadding.writeCSSString(buffer, "0")
|
||||||
|
buffer.WriteByte(';')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tabsBackground, ok := tabsLayout.session.Color("tabsBackgroundColor"); ok {
|
||||||
|
buffer.WriteString(` background-color: `)
|
||||||
|
buffer.WriteString(tabsBackground.cssString())
|
||||||
|
buffer.WriteByte(';')
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`">`)
|
||||||
|
|
||||||
|
inactiveStyle := tabsLayout.inactiveTabStyle()
|
||||||
|
activeStyle := tabsLayout.activeTabStyle()
|
||||||
|
|
||||||
|
notTranslate := GetNotTranslate(tabsLayout, "")
|
||||||
|
last := len(tabsLayout.views) - 1
|
||||||
|
for n, view := range tabsLayout.views {
|
||||||
|
title, _ := stringProperty(view, "title", tabsLayout.session)
|
||||||
|
if !notTranslate {
|
||||||
|
title, _ = tabsLayout.Session().GetString(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`<div id="`)
|
||||||
|
buffer.WriteString(tabsLayoutID)
|
||||||
|
buffer.WriteByte('-')
|
||||||
|
buffer.WriteString(strconv.Itoa(n))
|
||||||
|
buffer.WriteString(`" class="`)
|
||||||
|
if n == current {
|
||||||
|
buffer.WriteString(activeStyle)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(inactiveStyle)
|
||||||
|
}
|
||||||
|
buffer.WriteString(`" tabindex="0" onclick="tabClickEvent(\'`)
|
||||||
|
buffer.WriteString(tabsLayoutID)
|
||||||
|
buffer.WriteString(`\', `)
|
||||||
|
buffer.WriteString(strconv.Itoa(n))
|
||||||
|
buffer.WriteString(`, event)`)
|
||||||
|
buffer.WriteString(`" onclick="tabKeyClickEvent(\'`)
|
||||||
|
buffer.WriteString(tabsLayoutID)
|
||||||
|
buffer.WriteString(`\', `)
|
||||||
|
buffer.WriteString(strconv.Itoa(n))
|
||||||
|
buffer.WriteString(`, event)" style="display: flex; flex-flow: row nowrap; justify-content: center; align-items: center; `)
|
||||||
|
|
||||||
|
if n != last && tabsSpace.Type != Auto && tabsSpace.Value > 0 {
|
||||||
|
if rowLayout {
|
||||||
|
buffer.WriteString(` margin-right: `)
|
||||||
|
buffer.WriteString(tabsSpace.cssString(""))
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` margin-bottom: `)
|
||||||
|
buffer.WriteString(tabsSpace.cssString(""))
|
||||||
|
}
|
||||||
|
buffer.WriteByte(';')
|
||||||
|
}
|
||||||
|
|
||||||
|
switch location {
|
||||||
|
case LeftListTabs, RightListTabs:
|
||||||
|
if tabsHeight.Type != Auto {
|
||||||
|
buffer.WriteString(` height: `)
|
||||||
|
buffer.WriteString(tabsHeight.cssString(""))
|
||||||
|
buffer.WriteByte(';')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`" data-container="`)
|
||||||
|
buffer.WriteString(tabsLayoutID)
|
||||||
|
buffer.WriteString(`" data-view="`)
|
||||||
|
//buffer.WriteString(view.htmlID())
|
||||||
|
buffer.WriteString(tabsLayoutID)
|
||||||
|
buffer.WriteString(`-page`)
|
||||||
|
buffer.WriteString(strconv.Itoa(n))
|
||||||
|
buffer.WriteString(`"><div`)
|
||||||
|
|
||||||
|
switch location {
|
||||||
|
case LeftTabs:
|
||||||
|
buffer.WriteString(` style="writing-mode: vertical-lr; transform: rotate(180deg)">`)
|
||||||
|
|
||||||
|
case RightTabs:
|
||||||
|
buffer.WriteString(` style="writing-mode: vertical-lr;">`)
|
||||||
|
|
||||||
|
default:
|
||||||
|
buffer.WriteByte('>')
|
||||||
|
}
|
||||||
|
buffer.WriteString(title)
|
||||||
|
buffer.WriteString(`</div></div>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`</div>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, view := range tabsLayout.views {
|
||||||
|
buffer.WriteString(`<div id="`)
|
||||||
|
buffer.WriteString(tabsLayoutID)
|
||||||
|
buffer.WriteString(`-page`)
|
||||||
|
buffer.WriteString(strconv.Itoa(n))
|
||||||
|
|
||||||
|
switch location {
|
||||||
|
case LeftTabs, LeftListTabs:
|
||||||
|
buffer.WriteString(`" style="position: relative; grid-row-start: 1; grid-row-end: 2; grid-column-start: 2; grid-column-end: 3;`)
|
||||||
|
|
||||||
|
case TopTabs:
|
||||||
|
buffer.WriteString(`" style="position: relative; grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;`)
|
||||||
|
|
||||||
|
default:
|
||||||
|
buffer.WriteString(`" style="position: relative; grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if current != n {
|
||||||
|
buffer.WriteString(` display: none;`)
|
||||||
|
}
|
||||||
|
buffer.WriteString(`">`)
|
||||||
|
|
||||||
|
view.addToCSSStyle(map[string]string{`position`: `absolute`, `left`: `0`, `right`: `0`, `top`: `0`, `bottom`: `0`})
|
||||||
|
viewHTML(tabsLayout.views[n], buffer)
|
||||||
|
buffer.WriteString(`</div>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tabsLayout *tabsLayoutData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
switch command {
|
||||||
|
case "tabClick":
|
||||||
|
if numberText, ok := data.PropertyValue("number"); ok {
|
||||||
|
if number, err := strconv.Atoi(numberText); err == nil {
|
||||||
|
current := tabsLayout.currentItem()
|
||||||
|
if current != number {
|
||||||
|
tabsLayout.properties[Current] = number
|
||||||
|
if tabsLayout.tabListener != nil {
|
||||||
|
oldView := tabsLayout.views[current]
|
||||||
|
view := tabsLayout.views[number]
|
||||||
|
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, number, view, current, oldView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return tabsLayout.viewsContainerData.handleCommand(self, command, data)
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextView - text View
|
||||||
|
type TextView interface {
|
||||||
|
View
|
||||||
|
}
|
||||||
|
|
||||||
|
type textViewData struct {
|
||||||
|
viewData
|
||||||
|
// TODO textShadow
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTextView create new TextView object and return it
|
||||||
|
func NewTextView(session Session, params Params) TextView {
|
||||||
|
view := new(textViewData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTextView(session Session) View {
|
||||||
|
return NewTextView(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initialize fields of TextView by default values
|
||||||
|
func (textView *textViewData) Init(session Session) {
|
||||||
|
textView.viewData.Init(session)
|
||||||
|
textView.tag = "TextView"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textView *textViewData) Get(tag string) interface{} {
|
||||||
|
return textView.get(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textView *textViewData) Remove(tag string) {
|
||||||
|
textView.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textView *textViewData) remove(tag string) {
|
||||||
|
textView.viewData.remove(tag)
|
||||||
|
switch tag {
|
||||||
|
case Text:
|
||||||
|
updateInnerHTML(textView.htmlID(), textView.session)
|
||||||
|
|
||||||
|
case TextOverflow:
|
||||||
|
textView.textOverflowUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textView *textViewData) Set(tag string, value interface{}) bool {
|
||||||
|
return textView.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textView *textViewData) set(tag string, value interface{}) bool {
|
||||||
|
switch tag {
|
||||||
|
case Text:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
textView.properties[Text] = value
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
textView.properties[Text] = value.String()
|
||||||
|
|
||||||
|
case float32:
|
||||||
|
textView.properties[Text] = fmt.Sprintf("%g", float64(value))
|
||||||
|
|
||||||
|
case float64:
|
||||||
|
textView.properties[Text] = fmt.Sprintf("%g", value)
|
||||||
|
|
||||||
|
case []rune:
|
||||||
|
textView.properties[Text] = string(value)
|
||||||
|
|
||||||
|
case bool:
|
||||||
|
if value {
|
||||||
|
textView.properties[Text] = "true"
|
||||||
|
} else {
|
||||||
|
textView.properties[Text] = "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if n, ok := isInt(value); ok {
|
||||||
|
textView.properties[Text] = fmt.Sprintf("%d", n)
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateInnerHTML(textView.htmlID(), textView.session)
|
||||||
|
return true
|
||||||
|
|
||||||
|
case TextOverflow:
|
||||||
|
if textView.viewData.set(tag, value) {
|
||||||
|
textView.textOverflowUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return textView.viewData.set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textView *textViewData) textOverflowUpdated() {
|
||||||
|
session := textView.Session()
|
||||||
|
if n, ok := enumProperty(textView, TextOverflow, textView.session, 0); ok {
|
||||||
|
values := enumProperties[TextOverflow].cssValues
|
||||||
|
if n >= 0 && n < len(values) {
|
||||||
|
updateCSSProperty(textView.htmlID(), TextOverflow, values[n], session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateCSSProperty(textView.htmlID(), TextOverflow, "", session)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (textView *textViewData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
if value, ok := stringProperty(textView, Text, textView.Session()); ok {
|
||||||
|
if !GetNotTranslate(textView, "") {
|
||||||
|
value, _ = textView.session.GetString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
text := strings.ReplaceAll(value, `"`, `\"`)
|
||||||
|
text = strings.ReplaceAll(text, "\n", `\n`)
|
||||||
|
text = strings.ReplaceAll(text, "\r", `\r`)
|
||||||
|
buffer.WriteString(strings.ReplaceAll(text, `'`, `\'`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTextOverflow returns a value of the "text-overflow" property:
|
||||||
|
// TextOverflowClip (0) or TextOverflowEllipsis (1).
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTextOverflow(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return SingleLineText
|
||||||
|
}
|
||||||
|
t, _ := enumStyledProperty(view, TextOverflow, SingleLineText)
|
||||||
|
return t
|
||||||
|
}
|
|
@ -0,0 +1,329 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultMedia = 0
|
||||||
|
portraitMedia = 1
|
||||||
|
landscapeMedia = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type mediaStyle struct {
|
||||||
|
orientation int
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
styles map[string]DataObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule mediaStyle) cssText() string {
|
||||||
|
builder := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(builder)
|
||||||
|
|
||||||
|
switch rule.orientation {
|
||||||
|
case portraitMedia:
|
||||||
|
builder.WriteString(" and (orientation: portrait)")
|
||||||
|
|
||||||
|
case landscapeMedia:
|
||||||
|
builder.WriteString(" and (orientation: landscape)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.width > 0 {
|
||||||
|
builder.WriteString(" and (max-width: ")
|
||||||
|
builder.WriteString(strconv.Itoa(rule.width))
|
||||||
|
builder.WriteString("px)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.height > 0 {
|
||||||
|
builder.WriteString(" and (max-height: ")
|
||||||
|
builder.WriteString(strconv.Itoa(rule.height))
|
||||||
|
builder.WriteString("px)")
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMediaRule(text string) (mediaStyle, bool) {
|
||||||
|
rule := mediaStyle{orientation: defaultMedia, width: 0, height: 0, styles: map[string]DataObject{}}
|
||||||
|
elements := strings.Split(text, ":")
|
||||||
|
for i := 1; i < len(elements); i++ {
|
||||||
|
switch element := elements[i]; element {
|
||||||
|
case "portrait":
|
||||||
|
if rule.orientation != defaultMedia {
|
||||||
|
ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`)
|
||||||
|
return rule, false
|
||||||
|
}
|
||||||
|
rule.orientation = portraitMedia
|
||||||
|
|
||||||
|
case "landscape":
|
||||||
|
if rule.orientation != defaultMedia {
|
||||||
|
ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`)
|
||||||
|
return rule, false
|
||||||
|
}
|
||||||
|
rule.orientation = landscapeMedia
|
||||||
|
|
||||||
|
default:
|
||||||
|
elementSize := func(name string) (int, bool) {
|
||||||
|
if strings.HasPrefix(element, name) {
|
||||||
|
size, err := strconv.Atoi(element[len(name):])
|
||||||
|
if err == nil && size > 0 {
|
||||||
|
return size, true
|
||||||
|
}
|
||||||
|
ErrorLogF(`Invalid style section name "%s": %s`, text, err.Error())
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return 0, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if size, ok := elementSize("width"); !ok || size > 0 {
|
||||||
|
if !ok {
|
||||||
|
return rule, false
|
||||||
|
}
|
||||||
|
if rule.width != 0 {
|
||||||
|
ErrorLog(`Duplicate "width" tag in the style section "` + text + `"`)
|
||||||
|
return rule, false
|
||||||
|
}
|
||||||
|
rule.width = size
|
||||||
|
} else if size, ok := elementSize("height"); !ok || size > 0 {
|
||||||
|
if !ok {
|
||||||
|
return rule, false
|
||||||
|
}
|
||||||
|
if rule.height != 0 {
|
||||||
|
ErrorLog(`Duplicate "height" tag in the style section "` + text + `"`)
|
||||||
|
return rule, false
|
||||||
|
}
|
||||||
|
rule.height = size
|
||||||
|
} else {
|
||||||
|
ErrorLogF(`Unknown elemnet "%s" in the style section name "%s"`, element, text)
|
||||||
|
return rule, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rule, true
|
||||||
|
}
|
||||||
|
|
||||||
|
type theme struct {
|
||||||
|
name string
|
||||||
|
constants map[string]string
|
||||||
|
touchConstants map[string]string
|
||||||
|
colors map[string]string
|
||||||
|
darkColors map[string]string
|
||||||
|
styles map[string]DataObject
|
||||||
|
mediaStyles []mediaStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultTheme = new(theme)
|
||||||
|
|
||||||
|
func newTheme(text string) (*theme, bool) {
|
||||||
|
result := new(theme)
|
||||||
|
result.init()
|
||||||
|
ok := result.addText(text)
|
||||||
|
return result, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (theme *theme) init() {
|
||||||
|
theme.constants = map[string]string{}
|
||||||
|
theme.touchConstants = map[string]string{}
|
||||||
|
theme.colors = map[string]string{}
|
||||||
|
theme.darkColors = map[string]string{}
|
||||||
|
theme.styles = map[string]DataObject{}
|
||||||
|
theme.mediaStyles = []mediaStyle{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (theme *theme) concat(anotherTheme *theme) {
|
||||||
|
if theme.constants == nil {
|
||||||
|
theme.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag, constant := range anotherTheme.constants {
|
||||||
|
theme.constants[tag] = constant
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag, constant := range anotherTheme.touchConstants {
|
||||||
|
theme.touchConstants[tag] = constant
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag, color := range anotherTheme.colors {
|
||||||
|
theme.colors[tag] = color
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag, color := range anotherTheme.darkColors {
|
||||||
|
theme.darkColors[tag] = color
|
||||||
|
}
|
||||||
|
|
||||||
|
for tag, style := range anotherTheme.styles {
|
||||||
|
theme.styles[tag] = style
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, anotherMedia := range anotherTheme.mediaStyles {
|
||||||
|
exists := false
|
||||||
|
for _, media := range theme.mediaStyles {
|
||||||
|
if anotherMedia.height == media.height &&
|
||||||
|
anotherMedia.width == media.width &&
|
||||||
|
anotherMedia.orientation == media.orientation {
|
||||||
|
for tag, style := range anotherMedia.styles {
|
||||||
|
media.styles[tag] = style
|
||||||
|
}
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
theme.mediaStyles = append(theme.mediaStyles, anotherMedia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (theme *theme) cssText(session Session) string {
|
||||||
|
if theme.styles == nil {
|
||||||
|
theme.init()
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder cssStyleBuilder
|
||||||
|
builder.init()
|
||||||
|
|
||||||
|
for tag, obj := range theme.styles {
|
||||||
|
var style viewStyle
|
||||||
|
style.init()
|
||||||
|
parseProperties(&style, obj)
|
||||||
|
builder.startStyle(tag)
|
||||||
|
style.cssViewStyle(&builder, session, nil)
|
||||||
|
builder.endStyle()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, media := range theme.mediaStyles {
|
||||||
|
builder.startMedia(media.cssText())
|
||||||
|
for tag, obj := range media.styles {
|
||||||
|
var style viewStyle
|
||||||
|
style.init()
|
||||||
|
parseProperties(&style, obj)
|
||||||
|
builder.startStyle(tag)
|
||||||
|
style.cssViewStyle(&builder, session, nil)
|
||||||
|
builder.endStyle()
|
||||||
|
}
|
||||||
|
builder.endMedia()
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (theme *theme) addText(themeText string) bool {
|
||||||
|
data := ParseDataText(themeText)
|
||||||
|
if data == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.addData(data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (theme *theme) addData(data DataObject) {
|
||||||
|
if theme.constants == nil {
|
||||||
|
theme.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.IsObject() && data.Tag() == "theme" {
|
||||||
|
theme.parseThemeData(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (theme *theme) parseThemeData(data DataObject) {
|
||||||
|
count := data.PropertyCount()
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
if d := data.Property(i); d != nil {
|
||||||
|
switch tag := d.Tag(); tag {
|
||||||
|
case "constants":
|
||||||
|
if d.Type() == ObjectNode {
|
||||||
|
if obj := d.Object(); obj != nil {
|
||||||
|
objCount := obj.PropertyCount()
|
||||||
|
for k := 0; k < objCount; k++ {
|
||||||
|
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
|
||||||
|
theme.constants[prop.Tag()] = prop.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "constants:touch":
|
||||||
|
if d.Type() == ObjectNode {
|
||||||
|
if obj := d.Object(); obj != nil {
|
||||||
|
objCount := obj.PropertyCount()
|
||||||
|
for k := 0; k < objCount; k++ {
|
||||||
|
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
|
||||||
|
theme.touchConstants[prop.Tag()] = prop.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "colors":
|
||||||
|
if d.Type() == ObjectNode {
|
||||||
|
if obj := d.Object(); obj != nil {
|
||||||
|
objCount := obj.PropertyCount()
|
||||||
|
for k := 0; k < objCount; k++ {
|
||||||
|
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
|
||||||
|
theme.colors[prop.Tag()] = prop.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "colors:dark":
|
||||||
|
if d.Type() == ObjectNode {
|
||||||
|
if obj := d.Object(); obj != nil {
|
||||||
|
objCount := obj.PropertyCount()
|
||||||
|
for k := 0; k < objCount; k++ {
|
||||||
|
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
|
||||||
|
theme.darkColors[prop.Tag()] = prop.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "styles":
|
||||||
|
if d.Type() == ArrayNode {
|
||||||
|
arraySize := d.ArraySize()
|
||||||
|
for k := 0; k < arraySize; k++ {
|
||||||
|
if element := d.ArrayElement(k); element != nil && element.IsObject() {
|
||||||
|
if obj := element.Object(); obj != nil {
|
||||||
|
theme.styles[obj.Tag()] = obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if d.Type() == ArrayNode && strings.HasPrefix(tag, "styles:") {
|
||||||
|
if rule, ok := parseMediaRule(tag); ok {
|
||||||
|
arraySize := d.ArraySize()
|
||||||
|
for k := 0; k < arraySize; k++ {
|
||||||
|
if element := d.ArrayElement(k); element != nil && element.IsObject() {
|
||||||
|
if obj := element.Object(); obj != nil {
|
||||||
|
rule.styles[obj.Tag()] = obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
theme.mediaStyles = append(theme.mediaStyles, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(theme.mediaStyles) > 0 {
|
||||||
|
sort.SliceStable(theme.mediaStyles, func(i, j int) bool {
|
||||||
|
if theme.mediaStyles[i].orientation != theme.mediaStyles[j].orientation {
|
||||||
|
return theme.mediaStyles[i].orientation < theme.mediaStyles[j].orientation
|
||||||
|
}
|
||||||
|
if theme.mediaStyles[i].width != theme.mediaStyles[j].width {
|
||||||
|
return theme.mediaStyles[i].width < theme.mediaStyles[j].width
|
||||||
|
}
|
||||||
|
return theme.mediaStyles[i].height < theme.mediaStyles[j].height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,410 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TimeChangedEvent = "time-changed"
|
||||||
|
TimePickerMin = "time-picker-min"
|
||||||
|
TimePickerMax = "time-picker-max"
|
||||||
|
TimePickerStep = "time-picker-step"
|
||||||
|
TimePickerValue = "time-picker-value"
|
||||||
|
timeFormat = "15:04"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimePicker - TimePicker view
|
||||||
|
type TimePicker interface {
|
||||||
|
View
|
||||||
|
}
|
||||||
|
|
||||||
|
type timePickerData struct {
|
||||||
|
viewData
|
||||||
|
timeChangedListeners []func(TimePicker, time.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTimePicker create new TimePicker object and return it
|
||||||
|
func NewTimePicker(session Session, params Params) TimePicker {
|
||||||
|
view := new(timePickerData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTimePicker(session Session) View {
|
||||||
|
return NewTimePicker(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) Init(session Session) {
|
||||||
|
picker.viewData.Init(session)
|
||||||
|
picker.tag = "TimePicker"
|
||||||
|
picker.timeChangedListeners = []func(TimePicker, time.Time){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) normalizeTag(tag string) string {
|
||||||
|
tag = strings.ToLower(tag)
|
||||||
|
switch tag {
|
||||||
|
case Type, Min, Max, Step, Value:
|
||||||
|
return "time-picker-" + tag
|
||||||
|
}
|
||||||
|
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) Remove(tag string) {
|
||||||
|
picker.remove(picker.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case TimeChangedEvent:
|
||||||
|
if len(picker.timeChangedListeners) > 0 {
|
||||||
|
picker.timeChangedListeners = []func(TimePicker, time.Time){}
|
||||||
|
}
|
||||||
|
|
||||||
|
case TimePickerMin:
|
||||||
|
delete(picker.properties, TimePickerMin)
|
||||||
|
removeProperty(picker.htmlID(), Min, picker.session)
|
||||||
|
|
||||||
|
case TimePickerMax:
|
||||||
|
delete(picker.properties, TimePickerMax)
|
||||||
|
removeProperty(picker.htmlID(), Max, picker.session)
|
||||||
|
|
||||||
|
case TimePickerStep:
|
||||||
|
delete(picker.properties, TimePickerMax)
|
||||||
|
removeProperty(picker.htmlID(), Step, picker.session)
|
||||||
|
|
||||||
|
case TimePickerValue:
|
||||||
|
delete(picker.properties, TimePickerValue)
|
||||||
|
updateProperty(picker.htmlID(), Value, time.Now().Format(timeFormat), picker.session)
|
||||||
|
|
||||||
|
default:
|
||||||
|
picker.viewData.remove(tag)
|
||||||
|
picker.propertyChanged(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) Set(tag string, value interface{}) bool {
|
||||||
|
return picker.set(picker.normalizeTag(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
picker.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeValue := func(tag string) (time.Time, bool) {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case time.Time:
|
||||||
|
picker.properties[tag] = value
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := picker.Session().resolveConstants(value); ok {
|
||||||
|
if time, err := time.Parse(timeFormat, text); err == nil {
|
||||||
|
picker.properties[tag] = value
|
||||||
|
return time, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case TimePickerMin:
|
||||||
|
old, oldOK := getTimeProperty(picker, TimePickerMin, Min)
|
||||||
|
if time, ok := setTimeValue(TimePickerMin); ok {
|
||||||
|
if !oldOK || time != old {
|
||||||
|
updateProperty(picker.htmlID(), Min, time.Format(timeFormat), picker.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case TimePickerMax:
|
||||||
|
old, oldOK := getTimeProperty(picker, TimePickerMax, Max)
|
||||||
|
if time, ok := setTimeValue(TimePickerMax); ok {
|
||||||
|
if !oldOK || time != old {
|
||||||
|
updateProperty(picker.htmlID(), Max, time.Format(timeFormat), picker.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case TimePickerStep:
|
||||||
|
oldStep := GetTimePickerStep(picker, "")
|
||||||
|
if picker.setIntProperty(TimePickerStep, value) {
|
||||||
|
step := GetTimePickerStep(picker, "")
|
||||||
|
if oldStep != step {
|
||||||
|
if step > 0 {
|
||||||
|
updateProperty(picker.htmlID(), Step, strconv.Itoa(step), picker.session)
|
||||||
|
} else {
|
||||||
|
removeProperty(picker.htmlID(), Step, picker.session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case TimePickerValue:
|
||||||
|
oldTime := GetTimePickerValue(picker, "")
|
||||||
|
if time, ok := setTimeValue(TimePickerMax); ok {
|
||||||
|
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), time.Format(timeFormat)))
|
||||||
|
if time != oldTime {
|
||||||
|
for _, listener := range picker.timeChangedListeners {
|
||||||
|
listener(picker, time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case TimeChangedEvent:
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(TimePicker, time.Time):
|
||||||
|
picker.timeChangedListeners = []func(TimePicker, time.Time){value}
|
||||||
|
|
||||||
|
case func(time.Time):
|
||||||
|
fn := func(view TimePicker, date time.Time) {
|
||||||
|
value(date)
|
||||||
|
}
|
||||||
|
picker.timeChangedListeners = []func(TimePicker, time.Time){fn}
|
||||||
|
|
||||||
|
case []func(TimePicker, time.Time):
|
||||||
|
picker.timeChangedListeners = value
|
||||||
|
|
||||||
|
case []func(time.Time):
|
||||||
|
listeners := make([]func(TimePicker, time.Time), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
listeners[i] = func(view TimePicker, date time.Time) {
|
||||||
|
val(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picker.timeChangedListeners = listeners
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
listeners := make([]func(TimePicker, time.Time), len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
if val == nil {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val := val.(type) {
|
||||||
|
case func(TimePicker, time.Time):
|
||||||
|
listeners[i] = val
|
||||||
|
|
||||||
|
case func(time.Time):
|
||||||
|
listeners[i] = func(view TimePicker, date time.Time) {
|
||||||
|
val(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
picker.timeChangedListeners = listeners
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
default:
|
||||||
|
if picker.viewData.set(tag, value) {
|
||||||
|
picker.propertyChanged(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) Get(tag string) interface{} {
|
||||||
|
return picker.get(picker.normalizeTag(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) get(tag string) interface{} {
|
||||||
|
switch tag {
|
||||||
|
case TimeChangedEvent:
|
||||||
|
return picker.timeChangedListeners
|
||||||
|
|
||||||
|
default:
|
||||||
|
return picker.viewData.get(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) htmlTag() string {
|
||||||
|
return "input"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
picker.viewData.htmlProperties(self, buffer)
|
||||||
|
|
||||||
|
buffer.WriteString(` type="time"`)
|
||||||
|
|
||||||
|
if min, ok := getTimeProperty(picker, TimePickerMin, Min); ok {
|
||||||
|
buffer.WriteString(` min="`)
|
||||||
|
buffer.WriteString(min.Format(timeFormat))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
if max, ok := getTimeProperty(picker, TimePickerMax, Max); ok {
|
||||||
|
buffer.WriteString(` max="`)
|
||||||
|
buffer.WriteString(max.Format(timeFormat))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
if step, ok := intProperty(picker, TimePickerStep, picker.Session(), 0); ok && step > 0 {
|
||||||
|
buffer.WriteString(` step="`)
|
||||||
|
buffer.WriteString(strconv.Itoa(step))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(` value="`)
|
||||||
|
buffer.WriteString(GetTimePickerValue(picker, "").Format(timeFormat))
|
||||||
|
buffer.WriteByte('"')
|
||||||
|
|
||||||
|
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||||
|
if IsDisabled(self) {
|
||||||
|
buffer.WriteString(` disabled`)
|
||||||
|
}
|
||||||
|
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (picker *timePickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
switch command {
|
||||||
|
case "textChanged":
|
||||||
|
if text, ok := data.PropertyValue("text"); ok {
|
||||||
|
if value, err := time.Parse(timeFormat, text); err == nil {
|
||||||
|
oldValue := GetTimePickerValue(picker, "")
|
||||||
|
picker.properties[TimePickerValue] = value
|
||||||
|
if value != oldValue {
|
||||||
|
for _, listener := range picker.timeChangedListeners {
|
||||||
|
listener(picker, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return picker.viewData.handleCommand(self, command, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeProperty(view View, mainTag, shortTag string) (time.Time, bool) {
|
||||||
|
valueToTime := func(value interface{}) (time.Time, bool) {
|
||||||
|
if value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case time.Time:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := view.Session().resolveConstants(value); ok {
|
||||||
|
if result, err := time.Parse(timeFormat, text); err == nil {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
if view != nil {
|
||||||
|
if result, ok := valueToTime(view.getRaw(mainTag)); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := valueFromStyle(view, shortTag); ok {
|
||||||
|
if result, ok := valueToTime(value); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimePickerMin returns the min time of TimePicker subview and "true" as the second value if the min time is set,
|
||||||
|
// "false" as the second value otherwise.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTimePickerMin(view View, subviewID string) (time.Time, bool) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
return getTimeProperty(view, TimePickerMin, Min)
|
||||||
|
}
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimePickerMax returns the max time of TimePicker subview and "true" as the second value if the min time is set,
|
||||||
|
// "false" as the second value otherwise.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTimePickerMax(view View, subviewID string) (time.Time, bool) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
return getTimeProperty(view, TimePickerMax, Max)
|
||||||
|
}
|
||||||
|
return time.Now(), false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimePickerStep returns the time changing step in seconds of TimePicker subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTimePickerStep(view View, subviewID string) int {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
|
||||||
|
result, ok := intStyledProperty(view, TimePickerStep, 60)
|
||||||
|
if !ok {
|
||||||
|
result, _ = intStyledProperty(view, Step, 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result < 0 {
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimePickerValue returns the time of TimePicker subview.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTimePickerValue(view View, subviewID string) time.Time {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view == nil {
|
||||||
|
return time.Now()
|
||||||
|
}
|
||||||
|
time, _ := getTimeProperty(view, TimePickerValue, Value)
|
||||||
|
return time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeChangedListeners returns the TimeChangedListener list of an TimePicker subview.
|
||||||
|
// If there are no listeners then the empty list is returned
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTimeChangedListeners(view View, subviewID string) []func(TimePicker, time.Time) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(TimeChangedEvent); value != nil {
|
||||||
|
if listeners, ok := value.([]func(TimePicker, time.Time)); ok {
|
||||||
|
return listeners
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(TimePicker, time.Time){}
|
||||||
|
}
|
|
@ -0,0 +1,347 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// TouchStart is the constant for "touch-start" property tag.
|
||||||
|
// The "touch-start" event is fired when one or more touch points are placed on the touch surface.
|
||||||
|
// The main listener format: func(View, TouchEvent).
|
||||||
|
// The additional listener formats: func(TouchEvent), func(View), and func().
|
||||||
|
TouchStart = "touch-start"
|
||||||
|
|
||||||
|
// TouchEnd is the constant for "touch-end" property tag.
|
||||||
|
// The "touch-end" event fires when one or more touch points are removed from the touch surface.
|
||||||
|
// The main listener format: func(View, TouchEvent).
|
||||||
|
// The additional listener formats: func(TouchEvent), func(View), and func().
|
||||||
|
TouchEnd = "touch-end"
|
||||||
|
|
||||||
|
// TouchMove is the constant for "touch-move" property tag.
|
||||||
|
// The "touch-move" event is fired when one or more touch points are moved along the touch surface.
|
||||||
|
// The main listener format: func(View, TouchEvent).
|
||||||
|
// The additional listener formats: func(TouchEvent), func(View), and func().
|
||||||
|
TouchMove = "touch-move"
|
||||||
|
|
||||||
|
// TouchCancel is the constant for "touch-cancel" property tag.
|
||||||
|
// The "touch-cancel" event is fired when one or more touch points have been disrupted
|
||||||
|
// in an implementation-specific manner (for example, too many touch points are created).
|
||||||
|
// The main listener format: func(View, TouchEvent).
|
||||||
|
// The additional listener formats: func(TouchEvent), func(View), and func().
|
||||||
|
TouchCancel = "touch-cancel"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Touch contains parameters of a single touch of a touch event
|
||||||
|
type Touch struct {
|
||||||
|
// Identifier is a unique identifier for this Touch object. A given touch point (say, by a finger)
|
||||||
|
// will have the same identifier for the duration of its movement around the surface.
|
||||||
|
// This lets you ensure that you're tracking the same touch all the time.
|
||||||
|
Identifier int
|
||||||
|
|
||||||
|
// X provides the horizontal coordinate within the view's viewport.
|
||||||
|
X float64
|
||||||
|
// Y provides the vertical coordinate within the view's viewport.
|
||||||
|
Y float64
|
||||||
|
|
||||||
|
// ClientX provides the horizontal coordinate within the application's viewport at which the event occurred.
|
||||||
|
ClientX float64
|
||||||
|
// ClientY provides the vertical coordinate within the application's viewport at which the event occurred.
|
||||||
|
ClientY float64
|
||||||
|
|
||||||
|
// ScreenX provides the horizontal coordinate (offset) of the touch pointer in global (screen) coordinates.
|
||||||
|
ScreenX float64
|
||||||
|
// ScreenY provides the vertical coordinate (offset) of the touch pointer in global (screen) coordinates.
|
||||||
|
ScreenY float64
|
||||||
|
|
||||||
|
// RadiusX is the X radius of the ellipse that most closely circumscribes the area of contact with the screen.
|
||||||
|
// The value is in pixels of the same scale as screenX.
|
||||||
|
RadiusX float64
|
||||||
|
// RadiusY is the Y radius of the ellipse that most closely circumscribes the area of contact with the screen.
|
||||||
|
// The value is in pixels of the same scale as screenX.
|
||||||
|
RadiusY float64
|
||||||
|
|
||||||
|
// RotationAngle is the angle (in degrees) that the ellipse described by radiusX and radiusY must be rotated,
|
||||||
|
// clockwise, to most accurately cover the area of contact between the user and the surface.
|
||||||
|
RotationAngle float64
|
||||||
|
|
||||||
|
// Force is the amount of pressure being applied to the surface by the user, as a float
|
||||||
|
// between 0.0 (no pressure) and 1.0 (maximum pressure).
|
||||||
|
Force float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// TouchEvent contains parameters of a touch event
|
||||||
|
type TouchEvent struct {
|
||||||
|
// TimeStamp is the time at which the event was created (in milliseconds).
|
||||||
|
// This value is time since epoch—but in reality, browsers' definitions vary.
|
||||||
|
TimeStamp uint64
|
||||||
|
|
||||||
|
// Touches is the array of all the Touch objects representing all current points
|
||||||
|
// of contact with the surface, regardless of target or changed status.
|
||||||
|
Touches []Touch
|
||||||
|
|
||||||
|
// CtrlKey == true if the control key was down when the event was fired. false otherwise.
|
||||||
|
CtrlKey bool
|
||||||
|
// ShiftKey == true if the shift key was down when the event was fired. false otherwise.
|
||||||
|
ShiftKey bool
|
||||||
|
// AltKey == true if the alt key was down when the event was fired. false otherwise.
|
||||||
|
AltKey bool
|
||||||
|
// MetaKey == true if the meta key was down when the event was fired. false otherwise.
|
||||||
|
MetaKey bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func valueToTouchListeners(value interface{}) ([]func(View, TouchEvent), bool) {
|
||||||
|
if value == nil {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := value.(type) {
|
||||||
|
case func(View, TouchEvent):
|
||||||
|
return []func(View, TouchEvent){value}, true
|
||||||
|
|
||||||
|
case func(TouchEvent):
|
||||||
|
fn := func(view View, event TouchEvent) {
|
||||||
|
value(event)
|
||||||
|
}
|
||||||
|
return []func(View, TouchEvent){fn}, true
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
fn := func(view View, event TouchEvent) {
|
||||||
|
value(view)
|
||||||
|
}
|
||||||
|
return []func(View, TouchEvent){fn}, true
|
||||||
|
|
||||||
|
case func():
|
||||||
|
fn := func(view View, event TouchEvent) {
|
||||||
|
value()
|
||||||
|
}
|
||||||
|
return []func(View, TouchEvent){fn}, true
|
||||||
|
|
||||||
|
case []func(View, TouchEvent):
|
||||||
|
if len(value) == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
for _, fn := range value {
|
||||||
|
if fn == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case []func(TouchEvent):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, TouchEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event TouchEvent) {
|
||||||
|
v(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []func(View):
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, TouchEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event TouchEvent) {
|
||||||
|
v(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []func():
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, TouchEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
listeners[i] = func(view View, event TouchEvent) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
|
||||||
|
case []interface{}:
|
||||||
|
count := len(value)
|
||||||
|
if count == 0 {
|
||||||
|
return nil, true
|
||||||
|
}
|
||||||
|
listeners := make([]func(View, TouchEvent), count)
|
||||||
|
for i, v := range value {
|
||||||
|
if v == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
switch v := v.(type) {
|
||||||
|
case func(View, TouchEvent):
|
||||||
|
listeners[i] = v
|
||||||
|
|
||||||
|
case func(TouchEvent):
|
||||||
|
listeners[i] = func(view View, event TouchEvent) {
|
||||||
|
v(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func(View):
|
||||||
|
listeners[i] = func(view View, event TouchEvent) {
|
||||||
|
v(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
case func():
|
||||||
|
listeners[i] = func(view View, event TouchEvent) {
|
||||||
|
v()
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listeners, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var touchEvents = map[string]struct{ jsEvent, jsFunc string }{
|
||||||
|
TouchStart: {jsEvent: "ontouchstart", jsFunc: "touchStartEvent"},
|
||||||
|
TouchEnd: {jsEvent: "ontouchend", jsFunc: "touchEndEvent"},
|
||||||
|
TouchMove: {jsEvent: "ontouchmove", jsFunc: "touchMoveEvent"},
|
||||||
|
TouchCancel: {jsEvent: "ontouchcancel", jsFunc: "touchCancelEvent"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setTouchListener(tag string, value interface{}) bool {
|
||||||
|
listeners, ok := valueToTouchListeners(value)
|
||||||
|
if !ok {
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if listeners == nil {
|
||||||
|
view.removeTouchListener(tag)
|
||||||
|
} else if js, ok := touchEvents[tag]; ok {
|
||||||
|
view.properties[tag] = listeners
|
||||||
|
if view.created {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) removeTouchListener(tag string) {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
if view.created {
|
||||||
|
if js, ok := touchEvents[tag]; ok {
|
||||||
|
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTouchListeners(view View, subviewID string, tag string) []func(View, TouchEvent) {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.Get(tag); value != nil {
|
||||||
|
if result, ok := value.([]func(View, TouchEvent)); ok {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []func(View, TouchEvent){}
|
||||||
|
}
|
||||||
|
|
||||||
|
func touchEventsHtml(view View, buffer *strings.Builder) {
|
||||||
|
for tag, js := range touchEvents {
|
||||||
|
if value := view.getRaw(tag); value != nil {
|
||||||
|
if listeners, ok := value.([]func(View, TouchEvent)); ok && len(listeners) > 0 {
|
||||||
|
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (event *TouchEvent) init(data DataObject) {
|
||||||
|
|
||||||
|
event.Touches = []Touch{}
|
||||||
|
event.TimeStamp = getTimeStamp(data)
|
||||||
|
if node := data.PropertyWithTag("touches"); node != nil && node.Type() == ArrayNode {
|
||||||
|
for i := 0; i < node.ArraySize(); i++ {
|
||||||
|
if element := node.ArrayElement(i); element != nil && element.IsObject() {
|
||||||
|
if obj := element.Object(); obj != nil {
|
||||||
|
var touch Touch
|
||||||
|
if value, ok := obj.PropertyValue("identifier"); ok {
|
||||||
|
touch.Identifier, _ = strconv.Atoi(value)
|
||||||
|
}
|
||||||
|
touch.X = dataFloatProperty(obj, "x")
|
||||||
|
touch.Y = dataFloatProperty(obj, "y")
|
||||||
|
touch.ClientX = dataFloatProperty(obj, "clientX")
|
||||||
|
touch.ClientY = dataFloatProperty(obj, "clientY")
|
||||||
|
touch.ScreenX = dataFloatProperty(obj, "screenX")
|
||||||
|
touch.ScreenY = dataFloatProperty(obj, "screenY")
|
||||||
|
touch.RadiusX = dataFloatProperty(obj, "radiusX")
|
||||||
|
touch.RadiusY = dataFloatProperty(obj, "radiusY")
|
||||||
|
touch.RotationAngle = dataFloatProperty(obj, "rotationAngle")
|
||||||
|
touch.Force = dataFloatProperty(obj, "force")
|
||||||
|
event.Touches = append(event.Touches, touch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.CtrlKey = dataBoolProperty(data, "ctrlKey")
|
||||||
|
event.ShiftKey = dataBoolProperty(data, "shiftKey")
|
||||||
|
event.AltKey = dataBoolProperty(data, "altKey")
|
||||||
|
event.MetaKey = dataBoolProperty(data, "metaKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTouchEvents(view View, tag string, data DataObject) {
|
||||||
|
listeners := getTouchListeners(view, "", tag)
|
||||||
|
if len(listeners) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var event TouchEvent
|
||||||
|
event.init(data)
|
||||||
|
|
||||||
|
for _, listener := range listeners {
|
||||||
|
listener(view, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTouchStartListeners returns the "touch-start" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTouchStartListeners(view View, subviewID string) []func(View, TouchEvent) {
|
||||||
|
return getTouchListeners(view, subviewID, TouchStart)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTouchEndListeners returns the "touch-end" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTouchEndListeners(view View, subviewID string) []func(View, TouchEvent) {
|
||||||
|
return getTouchListeners(view, subviewID, TouchEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTouchMoveListeners returns the "touch-move" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTouchMoveListeners(view View, subviewID string) []func(View, TouchEvent) {
|
||||||
|
return getTouchListeners(view, subviewID, TouchMove)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTouchCancelListeners returns the "touch-cancel" listener list. If there are no listeners then the empty list is returned.
|
||||||
|
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
|
||||||
|
func GetTouchCancelListeners(view View, subviewID string) []func(View, TouchEvent) {
|
||||||
|
return getTouchListeners(view, subviewID, TouchCancel)
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var stringBuilders []*strings.Builder = make([]*strings.Builder, 4096)
|
||||||
|
var stringBuilderCount = 0
|
||||||
|
|
||||||
|
func allocStringBuilder() *strings.Builder {
|
||||||
|
for stringBuilderCount > 0 {
|
||||||
|
stringBuilderCount--
|
||||||
|
result := stringBuilders[stringBuilderCount]
|
||||||
|
if result != nil {
|
||||||
|
stringBuilders[stringBuilderCount] = nil
|
||||||
|
result.Reset()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := new(strings.Builder)
|
||||||
|
result.Grow(4096)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func freeStringBuilder(builder *strings.Builder) {
|
||||||
|
if builder != nil {
|
||||||
|
if stringBuilderCount == len(stringBuilders) {
|
||||||
|
stringBuilders = append(stringBuilders, builder)
|
||||||
|
} else {
|
||||||
|
stringBuilders[stringBuilderCount] = builder
|
||||||
|
}
|
||||||
|
stringBuilderCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLocalIP() string {
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, address := range addrs {
|
||||||
|
// check the address type and if it is not a loopback the display it
|
||||||
|
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||||
|
if ipnet.IP.To4() != nil {
|
||||||
|
return ipnet.IP.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "localhost"
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataIntProperty(data DataObject, tag string) int {
|
||||||
|
if value, ok := data.PropertyValue(tag); ok {
|
||||||
|
if n, err := strconv.Atoi(value); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataBoolProperty(data DataObject, tag string) bool {
|
||||||
|
if value, ok := data.PropertyValue(tag); ok && value == "1" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataFloatProperty(data DataObject, tag string) float64 {
|
||||||
|
if value, ok := data.PropertyValue(tag); ok {
|
||||||
|
if n, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
|
@ -0,0 +1,134 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VideoWidth is the constant for the "video-width" property tag of VideoPlayer.
|
||||||
|
// The "video-width" float property defines the width of the video's display area in pixels.
|
||||||
|
VideoWidth = "video-width"
|
||||||
|
// VideoHeight is the constant for the "video-height" property tag of VideoPlayer.
|
||||||
|
// The "video-height" float property defines the height of the video's display area in pixels.
|
||||||
|
VideoHeight = "video-height"
|
||||||
|
// Poster is the constant for the "poster" property tag of VideoPlayer.
|
||||||
|
// The "poster" property defines an URL for an image to be shown while the video is downloading.
|
||||||
|
// If this attribute isn't specified, nothing is displayed until the first frame is available,
|
||||||
|
// then the first frame is shown as the poster frame.
|
||||||
|
Poster = "poster"
|
||||||
|
)
|
||||||
|
|
||||||
|
type VideoPlayer interface {
|
||||||
|
MediaPlayer
|
||||||
|
}
|
||||||
|
|
||||||
|
type videoPlayerData struct {
|
||||||
|
mediaPlayerData
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVideoPlayer create new MediaPlayer object and return it
|
||||||
|
func NewVideoPlayer(session Session, params Params) MediaPlayer {
|
||||||
|
view := new(videoPlayerData)
|
||||||
|
view.Init(session)
|
||||||
|
view.tag = "VideoPlayer"
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func newVideoPlayer(session Session) View {
|
||||||
|
return NewVideoPlayer(session, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *videoPlayerData) Init(session Session) {
|
||||||
|
player.mediaPlayerData.Init(session)
|
||||||
|
player.tag = "VideoPlayer"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *videoPlayerData) htmlTag() string {
|
||||||
|
return "video"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *videoPlayerData) Remove(tag string) {
|
||||||
|
player.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *videoPlayerData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
|
||||||
|
case VideoWidth:
|
||||||
|
delete(player.properties, tag)
|
||||||
|
removeProperty(player.htmlID(), "width", player.Session())
|
||||||
|
|
||||||
|
case VideoHeight:
|
||||||
|
delete(player.properties, tag)
|
||||||
|
removeProperty(player.htmlID(), "height", player.Session())
|
||||||
|
|
||||||
|
case Poster:
|
||||||
|
delete(player.properties, tag)
|
||||||
|
removeProperty(player.htmlID(), Poster, player.Session())
|
||||||
|
|
||||||
|
default:
|
||||||
|
player.mediaPlayerData.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *videoPlayerData) Set(tag string, value interface{}) bool {
|
||||||
|
return player.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *videoPlayerData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
player.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if player.mediaPlayerData.set(tag, value) {
|
||||||
|
session := player.Session()
|
||||||
|
updateSize := func(cssTag string) {
|
||||||
|
if size, ok := floatProperty(player, tag, session, 0); ok {
|
||||||
|
if size > 0 {
|
||||||
|
updateProperty(player.htmlID(), cssTag, fmt.Sprintf("%g", size), session)
|
||||||
|
} else {
|
||||||
|
removeProperty(player.htmlID(), cssTag, session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case VideoWidth:
|
||||||
|
updateSize("width")
|
||||||
|
|
||||||
|
case VideoHeight:
|
||||||
|
updateSize("height")
|
||||||
|
|
||||||
|
case Poster:
|
||||||
|
if url, ok := stringProperty(player, Poster, session); ok {
|
||||||
|
updateProperty(player.htmlID(), Poster, url, session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (player *videoPlayerData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
player.mediaPlayerData.htmlProperties(self, buffer)
|
||||||
|
|
||||||
|
session := player.Session()
|
||||||
|
|
||||||
|
if size, ok := floatProperty(player, VideoWidth, session, 0); ok && size > 0 {
|
||||||
|
buffer.WriteString(fmt.Sprintf(` width="%g"`, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
if size, ok := floatProperty(player, VideoHeight, session, 0); ok && size > 0 {
|
||||||
|
buffer.WriteString(fmt.Sprintf(` height="%g"`, size))
|
||||||
|
}
|
||||||
|
|
||||||
|
if url, ok := stringProperty(player, Poster, session); ok && url != "" {
|
||||||
|
buffer.WriteString(` poster="`)
|
||||||
|
buffer.WriteString(url)
|
||||||
|
buffer.WriteString(`"`)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,760 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Frame - the location and size of a rectangle area
|
||||||
|
type Frame struct {
|
||||||
|
// Left - the left border
|
||||||
|
Left float64
|
||||||
|
// Top - the top border
|
||||||
|
Top float64
|
||||||
|
// Width - the width of a rectangle area
|
||||||
|
Width float64
|
||||||
|
// Height - the height of a rectangle area
|
||||||
|
Height float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right returns the right border
|
||||||
|
func (frame Frame) Right() float64 {
|
||||||
|
return frame.Left + frame.Width
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom returns the bottom border
|
||||||
|
func (frame Frame) Bottom() float64 {
|
||||||
|
return frame.Top + frame.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params defines a type of a parameters list
|
||||||
|
type Params map[string]interface{}
|
||||||
|
|
||||||
|
func (params Params) AllTags() []string {
|
||||||
|
tags := make([]string, 0, len(params))
|
||||||
|
for t := range params {
|
||||||
|
tags = append(tags, t)
|
||||||
|
}
|
||||||
|
sort.Strings(tags)
|
||||||
|
return tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// View - base view interface
|
||||||
|
type View interface {
|
||||||
|
Properties
|
||||||
|
fmt.Stringer
|
||||||
|
ruiStringer
|
||||||
|
|
||||||
|
// Init initializes fields of View by default values
|
||||||
|
Init(session Session)
|
||||||
|
// Session returns the current Session interface
|
||||||
|
Session() Session
|
||||||
|
// Parent returns the parent view
|
||||||
|
Parent() View
|
||||||
|
parentHTMLID() string
|
||||||
|
setParentID(parentID string)
|
||||||
|
// Tag returns the tag of View interface
|
||||||
|
Tag() string
|
||||||
|
// ID returns the id of the view
|
||||||
|
ID() string
|
||||||
|
// Focusable returns true if the view receives the focus
|
||||||
|
Focusable() bool
|
||||||
|
// Frame returns the location and size of the view in pixels
|
||||||
|
Frame() Frame
|
||||||
|
// Scroll returns the location size of the scrolable view in pixels
|
||||||
|
Scroll() Frame
|
||||||
|
// SetAnimated sets the value (second argument) of the property with name defined by the first argument.
|
||||||
|
// Return "true" if the value has been set, in the opposite case "false" are returned and
|
||||||
|
// a description of the error is written to the log
|
||||||
|
SetAnimated(tag string, value interface{}, animation Animation) bool
|
||||||
|
|
||||||
|
handleCommand(self View, command string, data DataObject) bool
|
||||||
|
//updateEventHandlers()
|
||||||
|
htmlClass(disabled bool) string
|
||||||
|
htmlTag() string
|
||||||
|
closeHTMLTag() bool
|
||||||
|
htmlID() string
|
||||||
|
htmlSubviews(self View, buffer *strings.Builder)
|
||||||
|
htmlProperties(self View, buffer *strings.Builder)
|
||||||
|
htmlDisabledProperties(self View, buffer *strings.Builder)
|
||||||
|
cssStyle(self View, builder cssBuilder)
|
||||||
|
addToCSSStyle(addCSS map[string]string)
|
||||||
|
|
||||||
|
onResize(self View, x, y, width, height float64)
|
||||||
|
onItemResize(self View, index int, x, y, width, height float64)
|
||||||
|
setNoResizeEvent()
|
||||||
|
isNoResizeEvent() bool
|
||||||
|
setScroll(x, y, width, height float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
// viewData - base implementation of View interface
|
||||||
|
type viewData struct {
|
||||||
|
viewStyle
|
||||||
|
session Session
|
||||||
|
tag string
|
||||||
|
viewID string
|
||||||
|
_htmlID string
|
||||||
|
parentID string
|
||||||
|
systemClass string
|
||||||
|
animation map[string]Animation
|
||||||
|
addCSS map[string]string
|
||||||
|
frame Frame
|
||||||
|
scroll Frame
|
||||||
|
noResizeEvent bool
|
||||||
|
created bool
|
||||||
|
//animation map[string]AnimationEndListener
|
||||||
|
}
|
||||||
|
|
||||||
|
func newView(session Session) View {
|
||||||
|
view := new(viewData)
|
||||||
|
view.Init(session)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func setInitParams(view View, params Params) {
|
||||||
|
if params != nil {
|
||||||
|
session := view.Session()
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
session.setIgnoreViewUpdates(true)
|
||||||
|
defer session.setIgnoreViewUpdates(false)
|
||||||
|
}
|
||||||
|
for _, tag := range params.AllTags() {
|
||||||
|
if value, ok := params[tag]; ok {
|
||||||
|
view.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewView create new View object and return it
|
||||||
|
func NewView(session Session, params Params) View {
|
||||||
|
view := new(viewData)
|
||||||
|
view.Init(session)
|
||||||
|
setInitParams(view, params)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Init(session Session) {
|
||||||
|
view.viewStyle.init()
|
||||||
|
view.tag = "View"
|
||||||
|
view.session = session
|
||||||
|
view.addCSS = map[string]string{}
|
||||||
|
//view.animation = map[string]AnimationEndListener{}
|
||||||
|
view.animation = map[string]Animation{}
|
||||||
|
view.noResizeEvent = false
|
||||||
|
view.created = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Session() Session {
|
||||||
|
return view.session
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Parent() View {
|
||||||
|
return view.session.viewByHTMLID(view.parentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) parentHTMLID() string {
|
||||||
|
return view.parentID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) setParentID(parentID string) {
|
||||||
|
view.parentID = parentID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Tag() string {
|
||||||
|
return view.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) ID() string {
|
||||||
|
return view.viewID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) ViewByID(id string) View {
|
||||||
|
if id == view.ID() {
|
||||||
|
if v := view.session.viewByHTMLID(view.htmlID()); v != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Focusable() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Remove(tag string) {
|
||||||
|
view.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case ID:
|
||||||
|
view.viewID = ""
|
||||||
|
|
||||||
|
case Style, StyleDisabled:
|
||||||
|
if _, ok := view.properties[tag]; ok {
|
||||||
|
delete(view.properties, tag)
|
||||||
|
updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view)), view.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case FocusEvent, LostFocusEvent:
|
||||||
|
view.removeFocusListener(tag)
|
||||||
|
|
||||||
|
case KeyDownEvent, KeyUpEvent:
|
||||||
|
view.removeKeyListener(tag)
|
||||||
|
|
||||||
|
case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent:
|
||||||
|
view.removeMouseListener(tag)
|
||||||
|
|
||||||
|
case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel:
|
||||||
|
view.removePointerListener(tag)
|
||||||
|
|
||||||
|
case TouchStart, TouchEnd, TouchMove, TouchCancel:
|
||||||
|
view.removeTouchListener(tag)
|
||||||
|
|
||||||
|
case ResizeEvent, ScrollEvent:
|
||||||
|
delete(view.properties, tag)
|
||||||
|
|
||||||
|
case Content:
|
||||||
|
if _, ok := view.properties[Content]; ok {
|
||||||
|
delete(view.properties, Content)
|
||||||
|
updateInnerHTML(view.htmlID(), view.session)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
view.viewStyle.remove(tag)
|
||||||
|
view.propertyChanged(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Set(tag string, value interface{}) bool {
|
||||||
|
return view.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
view.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case ID:
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
view.viewID = text
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
notCompatibleType(ID, value)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case Style, StyleDisabled:
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
view.properties[tag] = text
|
||||||
|
//updateInnerHTML(view.parentID, view.session)
|
||||||
|
if view.created {
|
||||||
|
updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view)), view.session)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
notCompatibleType(ID, value)
|
||||||
|
return false
|
||||||
|
|
||||||
|
case FocusEvent, LostFocusEvent:
|
||||||
|
return view.setFocusListener(tag, value)
|
||||||
|
|
||||||
|
case KeyDownEvent, KeyUpEvent:
|
||||||
|
return view.setKeyListener(tag, value)
|
||||||
|
|
||||||
|
case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent:
|
||||||
|
return view.setMouseListener(tag, value)
|
||||||
|
|
||||||
|
case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel:
|
||||||
|
return view.setPointerListener(tag, value)
|
||||||
|
|
||||||
|
case TouchStart, TouchEnd, TouchMove, TouchCancel:
|
||||||
|
return view.setTouchListener(tag, value)
|
||||||
|
|
||||||
|
case ResizeEvent, ScrollEvent:
|
||||||
|
return view.setFrameListener(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.viewStyle.set(tag, value) {
|
||||||
|
if view.created {
|
||||||
|
view.propertyChanged(tag)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) propertyChanged(tag string) {
|
||||||
|
|
||||||
|
if view.updateTransformProperty(tag) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlID := view.htmlID()
|
||||||
|
session := view.session
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Disabled:
|
||||||
|
updateInnerHTML(view.parentHTMLID(), session)
|
||||||
|
|
||||||
|
case Background:
|
||||||
|
updateCSSProperty(htmlID, Background, view.backgroundCSS(view), session)
|
||||||
|
return
|
||||||
|
|
||||||
|
case Border:
|
||||||
|
if getBorder(view, Border) == nil {
|
||||||
|
updateCSSProperty(htmlID, BorderWidth, "", session)
|
||||||
|
updateCSSProperty(htmlID, BorderColor, "", session)
|
||||||
|
updateCSSProperty(htmlID, BorderStyle, "none", session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
case BorderLeft, BorderRight, BorderTop, BorderBottom:
|
||||||
|
if border := getBorder(view, Border); border != nil {
|
||||||
|
updateCSSProperty(htmlID, BorderWidth, border.cssWidthValue(session), session)
|
||||||
|
updateCSSProperty(htmlID, BorderColor, border.cssColorValue(session), session)
|
||||||
|
updateCSSProperty(htmlID, BorderStyle, border.cssStyleValue(session), session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case BorderStyle, BorderLeftStyle, BorderRightStyle, BorderTopStyle, BorderBottomStyle:
|
||||||
|
if border := getBorder(view, Border); border != nil {
|
||||||
|
updateCSSProperty(htmlID, BorderStyle, border.cssStyleValue(session), session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case BorderColor, BorderLeftColor, BorderRightColor, BorderTopColor, BorderBottomColor:
|
||||||
|
if border := getBorder(view, Border); border != nil {
|
||||||
|
updateCSSProperty(htmlID, BorderColor, border.cssColorValue(session), session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case BorderWidth, BorderLeftWidth, BorderRightWidth, BorderTopWidth, BorderBottomWidth:
|
||||||
|
if border := getBorder(view, Border); border != nil {
|
||||||
|
updateCSSProperty(htmlID, BorderWidth, border.cssWidthValue(session), session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case Outline, OutlineColor, OutlineStyle, OutlineWidth:
|
||||||
|
updateCSSProperty(htmlID, Outline, GetOutline(view, "").cssString(), session)
|
||||||
|
return
|
||||||
|
|
||||||
|
case Shadow:
|
||||||
|
updateCSSProperty(htmlID, "box-shadow", shadowCSS(view, Shadow, session), session)
|
||||||
|
return
|
||||||
|
|
||||||
|
case TextShadow:
|
||||||
|
updateCSSProperty(htmlID, "text-shadow", shadowCSS(view, TextShadow, session), session)
|
||||||
|
return
|
||||||
|
|
||||||
|
case Radius, RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
|
||||||
|
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
|
||||||
|
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
|
||||||
|
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
|
||||||
|
radius := GetRadius(view, "")
|
||||||
|
updateCSSProperty(htmlID, "border-radius", radius.cssString(), session)
|
||||||
|
return
|
||||||
|
|
||||||
|
case Margin, MarginTop, MarginRight, MarginBottom, MarginLeft,
|
||||||
|
"top-margin", "right-margin", "bottom-margin", "left-margin":
|
||||||
|
margin := GetMargin(view, "")
|
||||||
|
updateCSSProperty(htmlID, Margin, margin.cssString(), session)
|
||||||
|
return
|
||||||
|
|
||||||
|
case Padding, PaddingTop, PaddingRight, PaddingBottom, PaddingLeft,
|
||||||
|
"top-padding", "right-padding", "bottom-padding", "left-padding":
|
||||||
|
padding := GetPadding(view, "")
|
||||||
|
updateCSSProperty(htmlID, Padding, padding.cssString(), session)
|
||||||
|
return
|
||||||
|
|
||||||
|
case AvoidBreak:
|
||||||
|
if avoid, ok := boolProperty(view, AvoidBreak, session); ok {
|
||||||
|
if avoid {
|
||||||
|
updateCSSProperty(htmlID, "break-inside", "avoid", session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, "break-inside", "auto", session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case Clip:
|
||||||
|
if clip := getClipShape(view, Clip, session); clip != nil && clip.valid(session) {
|
||||||
|
updateCSSProperty(htmlID, `clip-path`, clip.cssStyle(session), session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, `clip-path`, "none", session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case ShapeOutside:
|
||||||
|
if clip := getClipShape(view, ShapeOutside, session); clip != nil && clip.valid(session) {
|
||||||
|
updateCSSProperty(htmlID, ShapeOutside, clip.cssStyle(session), session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, ShapeOutside, "none", session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case Filter:
|
||||||
|
text := ""
|
||||||
|
if value := view.getRaw(Filter); value != nil {
|
||||||
|
if filter, ok := value.(ViewFilter); ok {
|
||||||
|
text = filter.cssStyle(session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateCSSProperty(htmlID, Filter, text, session)
|
||||||
|
return
|
||||||
|
|
||||||
|
case FontName:
|
||||||
|
if font, ok := stringProperty(view, FontName, session); ok {
|
||||||
|
updateCSSProperty(htmlID, "font-family", font, session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, "font-family", "", session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
case Italic:
|
||||||
|
if state, ok := boolProperty(view, tag, session); ok {
|
||||||
|
if state {
|
||||||
|
updateCSSProperty(htmlID, "font-style", "italic", session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, "font-style", "normal", session)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, "font-style", "", session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case SmallCaps:
|
||||||
|
if state, ok := boolProperty(view, tag, session); ok {
|
||||||
|
if state {
|
||||||
|
updateCSSProperty(htmlID, "font-variant", "small-caps", session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, "font-variant", "normal", session)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, "font-variant", "", session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Strikethrough, Overline, Underline:
|
||||||
|
updateCSSProperty(htmlID, "text-decoration", view.cssTextDecoration(session), session)
|
||||||
|
for _, tag2 := range []string{TextLineColor, TextLineStyle, TextLineThickness} {
|
||||||
|
view.propertyChanged(tag2)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if cssTag, ok := sizeProperties[tag]; ok {
|
||||||
|
size, _ := sizeProperty(view, tag, session)
|
||||||
|
updateCSSProperty(htmlID, cssTag, size.cssString(""), session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
colorTags := map[string]string{
|
||||||
|
BackgroundColor: BackgroundColor,
|
||||||
|
TextColor: "color",
|
||||||
|
TextLineColor: "text-decoration-color",
|
||||||
|
}
|
||||||
|
if cssTag, ok := colorTags[tag]; ok {
|
||||||
|
if color, ok := colorProperty(view, tag, session); ok {
|
||||||
|
updateCSSProperty(htmlID, cssTag, color.cssString(), session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, cssTag, "", session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" {
|
||||||
|
n, _ := enumProperty(view, tag, session, 0)
|
||||||
|
updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n], session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, floatTag := range []string{ScaleX, ScaleY, ScaleZ, RotateX, RotateY, RotateZ} {
|
||||||
|
if tag == floatTag {
|
||||||
|
if f, ok := floatProperty(view, floatTag, session, 0); ok {
|
||||||
|
updateCSSProperty(htmlID, floatTag, strconv.FormatFloat(f, 'g', -1, 64), session)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) Get(tag string) interface{} {
|
||||||
|
return view.get(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) get(tag string) interface{} {
|
||||||
|
return view.viewStyle.get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) htmlTag() string {
|
||||||
|
if semantics := GetSemantics(view, ""); semantics > DefaultSemantics {
|
||||||
|
values := enumProperties[Semantics].cssValues
|
||||||
|
if semantics < len(values) {
|
||||||
|
return values[semantics]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "div"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) closeHTMLTag() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) htmlID() string {
|
||||||
|
if view._htmlID == "" {
|
||||||
|
view._htmlID = view.session.nextViewID()
|
||||||
|
}
|
||||||
|
return view._htmlID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) addToCSSStyle(addCSS map[string]string) {
|
||||||
|
view.addCSS = addCSS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) cssStyle(self View, builder cssBuilder) {
|
||||||
|
view.viewStyle.cssViewStyle(builder, view.session, self)
|
||||||
|
switch GetVisibility(view, "") {
|
||||||
|
case Invisible:
|
||||||
|
builder.add(`visibility`, `hidden`)
|
||||||
|
|
||||||
|
case Gone:
|
||||||
|
builder.add(`display`, `none`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.addCSS != nil {
|
||||||
|
for tag, value := range view.addCSS {
|
||||||
|
builder.add(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) htmlProperties(self View, buffer *strings.Builder) {
|
||||||
|
view.created = true
|
||||||
|
if view.frame.Left != 0 || view.frame.Top != 0 || view.frame.Width != 0 || view.frame.Height != 0 {
|
||||||
|
buffer.WriteString(fmt.Sprintf(` data-left="%g" data-top="%g" data-width="%g" data-height="%g"`,
|
||||||
|
view.frame.Left, view.frame.Top, view.frame.Width, view.frame.Height))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||||
|
if IsDisabled(self) {
|
||||||
|
buffer.WriteString(` data-disabled="1"`)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(` data-disabled="0"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewHTML(view View, buffer *strings.Builder) {
|
||||||
|
viewHTMLTag := view.htmlTag()
|
||||||
|
buffer.WriteRune('<')
|
||||||
|
buffer.WriteString(viewHTMLTag)
|
||||||
|
buffer.WriteString(` id="`)
|
||||||
|
buffer.WriteString(view.htmlID())
|
||||||
|
buffer.WriteRune('"')
|
||||||
|
|
||||||
|
disabled := IsDisabled(view)
|
||||||
|
|
||||||
|
if cls := view.htmlClass(disabled); cls != "" {
|
||||||
|
buffer.WriteString(` class="`)
|
||||||
|
buffer.WriteString(cls)
|
||||||
|
buffer.WriteRune('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
var cssBuilder viewCSSBuilder
|
||||||
|
view.cssStyle(view, &cssBuilder)
|
||||||
|
|
||||||
|
if style := cssBuilder.finish(); style != "" {
|
||||||
|
buffer.WriteString(` style="`)
|
||||||
|
buffer.WriteString(style)
|
||||||
|
buffer.WriteRune('"')
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
view.htmlProperties(view, buffer)
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
view.htmlDisabledProperties(view, buffer)
|
||||||
|
|
||||||
|
if view.isNoResizeEvent() {
|
||||||
|
buffer.WriteString(` data-noresize="1" `)
|
||||||
|
} else {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.Focusable() && !disabled {
|
||||||
|
buffer.WriteString(`tabindex="0" `)
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(`onscroll="scrollEvent(this, event)" `)
|
||||||
|
|
||||||
|
keyEventsHtml(view, buffer)
|
||||||
|
mouseEventsHtml(view, buffer)
|
||||||
|
pointerEventsHtml(view, buffer)
|
||||||
|
touchEventsHtml(view, buffer)
|
||||||
|
focusEventsHtml(view, buffer)
|
||||||
|
|
||||||
|
buffer.WriteRune('>')
|
||||||
|
view.htmlSubviews(view, buffer)
|
||||||
|
if view.closeHTMLTag() {
|
||||||
|
buffer.WriteString(`</`)
|
||||||
|
buffer.WriteString(viewHTMLTag)
|
||||||
|
buffer.WriteRune('>')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) htmlClass(disabled bool) string {
|
||||||
|
cls := "ruiView"
|
||||||
|
disabledStyle := false
|
||||||
|
if disabled {
|
||||||
|
if value, ok := stringProperty(view, StyleDisabled, view.Session()); ok && value != "" {
|
||||||
|
cls += " " + value
|
||||||
|
disabledStyle = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !disabledStyle {
|
||||||
|
if value, ok := stringProperty(view, Style, view.Session()); ok {
|
||||||
|
cls += " " + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if view.systemClass != "" {
|
||||||
|
cls = view.systemClass + " " + cls
|
||||||
|
}
|
||||||
|
|
||||||
|
return cls
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) handleCommand(self View, command string, data DataObject) bool {
|
||||||
|
switch command {
|
||||||
|
|
||||||
|
case KeyDownEvent, KeyUpEvent:
|
||||||
|
if !IsDisabled(self) {
|
||||||
|
handleKeyEvents(self, command, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent:
|
||||||
|
handleMouseEvents(self, command, data)
|
||||||
|
|
||||||
|
case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel:
|
||||||
|
handlePointerEvents(self, command, data)
|
||||||
|
|
||||||
|
case TouchStart, TouchEnd, TouchMove, TouchCancel:
|
||||||
|
handleTouchEvents(self, command, data)
|
||||||
|
|
||||||
|
case FocusEvent, LostFocusEvent:
|
||||||
|
for _, listener := range getFocusListeners(view, "", command) {
|
||||||
|
listener(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "scroll":
|
||||||
|
view.onScroll(view, dataFloatProperty(data, "x"), dataFloatProperty(data, "y"), dataFloatProperty(data, "width"), dataFloatProperty(data, "height"))
|
||||||
|
|
||||||
|
case "widthChanged":
|
||||||
|
if value, ok := data.PropertyValue("width"); ok {
|
||||||
|
if width, ok := StringToSizeUnit(value); ok {
|
||||||
|
self.setRaw(Width, width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "heightChanged":
|
||||||
|
if value, ok := data.PropertyValue("height"); ok {
|
||||||
|
if height, ok := StringToSizeUnit(value); ok {
|
||||||
|
self.setRaw(Height, height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "transitionEnd":
|
||||||
|
if property, ok := data.PropertyValue("property"); ok {
|
||||||
|
if animation, ok := view.animation[property]; ok {
|
||||||
|
delete(view.animation, property)
|
||||||
|
view.updateTransitionCSS()
|
||||||
|
if animation.FinishListener != nil {
|
||||||
|
animation.FinishListener.OnAnimationFinished(self, property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
case "resize":
|
||||||
|
floatProperty := func(tag string) float64 {
|
||||||
|
if value, ok := data.PropertyValue(tag); ok {
|
||||||
|
if result, err := strconv.ParseFloat(value, 64); err == nil {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onResize(self, floatProperty("x"), floatProperty("y"), floatProperty("width"), floatProperty("height"))
|
||||||
|
return true
|
||||||
|
*/
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func ruiViewString(view View, viewTag string, writer ruiWriter) {
|
||||||
|
writer.startObject(viewTag)
|
||||||
|
|
||||||
|
tags := view.AllTags()
|
||||||
|
count := len(tags)
|
||||||
|
if count > 0 {
|
||||||
|
if count > 1 {
|
||||||
|
tagToStart := func(tag string) {
|
||||||
|
for i, t := range tags {
|
||||||
|
if t == tag {
|
||||||
|
if i > 0 {
|
||||||
|
for n := i; n > 0; n-- {
|
||||||
|
tags[n] = tags[n-1]
|
||||||
|
}
|
||||||
|
tags[0] = tag
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tagToStart(StyleDisabled)
|
||||||
|
tagToStart(Style)
|
||||||
|
tagToStart(ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
if value := view.Get(tag); value != nil {
|
||||||
|
writer.writeProperty(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) ruiString(writer ruiWriter) {
|
||||||
|
ruiViewString(view, view.Tag(), writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
view.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDisabled returns "true" if the view is disabled
|
||||||
|
func IsDisabled(view View) bool {
|
||||||
|
if disabled, _ := boolProperty(view, Disabled, view.Session()); disabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if parent := view.Parent(); parent != nil {
|
||||||
|
return IsDisabled(parent)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EaseTiming - a timing function which increases in velocity towards the middle of the transition, slowing back down at the end
|
||||||
|
EaseTiming = "ease"
|
||||||
|
// EaseInTiming - a timing function which starts off slowly, with the transition speed increasing until complete
|
||||||
|
EaseInTiming = "ease-in"
|
||||||
|
// EaseOutTiming - a timing function which starts transitioning quickly, slowing down the transition continues.
|
||||||
|
EaseOutTiming = "ease-out"
|
||||||
|
// EaseInOutTiming - a timing function which starts transitioning slowly, speeds up, and then slows down again.
|
||||||
|
EaseInOutTiming = "ease-in-out"
|
||||||
|
// LinearTiming - a timing function at an even speed
|
||||||
|
LinearTiming = "linear"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StepsTiming return a timing function along stepCount stops along the transition, diplaying each stop for equal lengths of time
|
||||||
|
func StepsTiming(stepCount int) string {
|
||||||
|
return "steps(" + strconv.Itoa(stepCount) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CubicBezierTiming return a cubic-Bezier curve timing function. x1 and x2 must be in the range [0, 1].
|
||||||
|
func CubicBezierTiming(x1, y1, x2, y2 float64) string {
|
||||||
|
if x1 < 0 {
|
||||||
|
x1 = 0
|
||||||
|
} else if x1 > 1 {
|
||||||
|
x1 = 1
|
||||||
|
}
|
||||||
|
if x2 < 0 {
|
||||||
|
x2 = 0
|
||||||
|
} else if x2 > 1 {
|
||||||
|
x2 = 1
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("cubic-bezier(%g, %g, %g, %g)", x1, y1, x2, y2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnimationFinishedListener describes the end of an animation event handler
|
||||||
|
type AnimationFinishedListener interface {
|
||||||
|
// OnAnimationFinished is called when a property animation is finished
|
||||||
|
OnAnimationFinished(view View, property string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Animation struct {
|
||||||
|
// Duration defines the time in seconds an animation should take to complete
|
||||||
|
Duration float64
|
||||||
|
// TimingFunction defines how intermediate values are calculated for a property being affected
|
||||||
|
// by an animation effect. If the value is "" then the "ease" function is used
|
||||||
|
TimingFunction string
|
||||||
|
// Delay defines the duration in seconds to wait before starting a property's animation.
|
||||||
|
Delay float64
|
||||||
|
// FinishListener defines the end of an animation event handler
|
||||||
|
FinishListener AnimationFinishedListener
|
||||||
|
}
|
||||||
|
|
||||||
|
type animationFinishedFunc struct {
|
||||||
|
finishFunc func(View, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (listener animationFinishedFunc) OnAnimationFinished(view View, property string) {
|
||||||
|
if listener.finishFunc != nil {
|
||||||
|
listener.finishFunc(view, property)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AnimationFinishedFunc(finishFunc func(View, string)) AnimationFinishedListener {
|
||||||
|
listener := new(animationFinishedFunc)
|
||||||
|
listener.finishFunc = finishFunc
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTimingFunction(timingFunction string) bool {
|
||||||
|
switch timingFunction {
|
||||||
|
case "", EaseTiming, EaseInTiming, EaseOutTiming, EaseInOutTiming, LinearTiming:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
size := len(timingFunction)
|
||||||
|
if size > 0 && timingFunction[size-1] == ')' {
|
||||||
|
if index := strings.IndexRune(timingFunction, '('); index > 0 {
|
||||||
|
args := timingFunction[index+1 : size-1]
|
||||||
|
switch timingFunction[:index] {
|
||||||
|
case "steps":
|
||||||
|
if _, err := strconv.Atoi(strings.Trim(args, " \t\n")); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case "cubic-bezier":
|
||||||
|
if params := strings.Split(args, ","); len(params) == 4 {
|
||||||
|
for _, param := range params {
|
||||||
|
if _, err := strconv.ParseFloat(strings.Trim(param, " \t\n"), 64); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) SetAnimated(tag string, value interface{}, animation Animation) bool {
|
||||||
|
timingFunction, ok := view.session.resolveConstants(animation.TimingFunction)
|
||||||
|
if !ok || animation.Duration <= 0 || !validateTimingFunction(timingFunction) {
|
||||||
|
if view.Set(tag, value) {
|
||||||
|
if animation.FinishListener != nil {
|
||||||
|
animation.FinishListener.OnAnimationFinished(view, tag)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProperty(view.htmlID(), "ontransitionend", "transitionEndEvent(this, event)", view.session)
|
||||||
|
updateProperty(view.htmlID(), "ontransitioncancel", "transitionCancelEvent(this, event)", view.session)
|
||||||
|
animation.TimingFunction = timingFunction
|
||||||
|
view.animation[tag] = animation
|
||||||
|
view.updateTransitionCSS()
|
||||||
|
|
||||||
|
result := view.Set(tag, value)
|
||||||
|
if !result {
|
||||||
|
delete(view.animation, tag)
|
||||||
|
view.updateTransitionCSS()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) transitionCSS() string {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
for tag, animation := range view.animation {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
}
|
||||||
|
buffer.WriteString(tag)
|
||||||
|
buffer.WriteString(fmt.Sprintf(" %gs", animation.Duration))
|
||||||
|
if animation.TimingFunction != "" {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(animation.TimingFunction)
|
||||||
|
}
|
||||||
|
if animation.Delay > 0 {
|
||||||
|
if animation.TimingFunction == "" {
|
||||||
|
buffer.WriteString(" ease")
|
||||||
|
}
|
||||||
|
buffer.WriteString(fmt.Sprintf(" %gs", animation.Delay))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) updateTransitionCSS() {
|
||||||
|
updateCSSProperty(view.htmlID(), "transition", view.transitionCSS(), view.Session())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAnimated sets the property with name "tag" of the "rootView" subview with "viewID" id by value. Result:
|
||||||
|
// true - success,
|
||||||
|
// false - error (incompatible type or invalid format of a string value, see AppLog).
|
||||||
|
func SetAnimated(rootView View, viewID, tag string, value interface{}, animation Animation) bool {
|
||||||
|
if view := ViewByID(rootView, viewID); view != nil {
|
||||||
|
return view.SetAnimated(tag, value, animation)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,266 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
// ViewByID return a View with id equal to the argument of the function or nil if there is no such View
|
||||||
|
func ViewByID(rootView View, id string) View {
|
||||||
|
if rootView == nil {
|
||||||
|
ErrorLog(`ViewByID(nil, "` + id + `"): rootView is nil`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if rootView.ID() == id {
|
||||||
|
return rootView
|
||||||
|
}
|
||||||
|
if container, ok := rootView.(ParanetView); ok {
|
||||||
|
if view := viewByID(container, id); view != nil {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrorLog(`ViewByID(_, "` + id + `"): View not found`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func viewByID(rootView ParanetView, id string) View {
|
||||||
|
for _, view := range rootView.Views() {
|
||||||
|
if view != nil {
|
||||||
|
if view.ID() == id {
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
if container, ok := view.(ParanetView); ok {
|
||||||
|
if v := viewByID(container, id); v != nil {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewsContainerByID return a ViewsContainer with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not ViewsContainer
|
||||||
|
func ViewsContainerByID(rootView View, id string) ViewsContainer {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if list, ok := view.(ViewsContainer); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
ErrorLog(`ViewsContainerByID(_, "` + id + `"): The found View is not ViewsContainer`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListLayoutByID return a ListLayout with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not ListLayout
|
||||||
|
func ListLayoutByID(rootView View, id string) ListLayout {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if list, ok := view.(ListLayout); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
ErrorLog(`ListLayoutByID(_, "` + id + `"): The found View is not ListLayout`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackLayoutByID return a StackLayout with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not StackLayout
|
||||||
|
func StackLayoutByID(rootView View, id string) StackLayout {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if list, ok := view.(StackLayout); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
ErrorLog(`StackLayoutByID(_, "` + id + `"): The found View is not StackLayout`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GridLayoutByID return a GridLayout with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not GridLayout
|
||||||
|
func GridLayoutByID(rootView View, id string) GridLayout {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if list, ok := view.(GridLayout); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
ErrorLog(`GridLayoutByID(_, "` + id + `"): The found View is not GridLayout`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColumnLayoutByID return a ColumnLayout with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not ColumnLayout
|
||||||
|
func ColumnLayoutByID(rootView View, id string) ColumnLayout {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if list, ok := view.(ColumnLayout); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
ErrorLog(`ColumnLayoutByID(_, "` + id + `"): The found View is not ColumnLayout`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetailsViewByID return a ColumnLayout with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not DetailsView
|
||||||
|
func DetailsViewByID(rootView View, id string) DetailsView {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if details, ok := view.(DetailsView); ok {
|
||||||
|
return details
|
||||||
|
}
|
||||||
|
ErrorLog(`DetailsViewByID(_, "` + id + `"): The found View is not DetailsView`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropDownListByID return a DropDownListView with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not DropDownListView
|
||||||
|
func DropDownListByID(rootView View, id string) DropDownList {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if list, ok := view.(DropDownList); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
ErrorLog(`DropDownListByID(_, "` + id + `"): The found View is not DropDownList`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TabsLayoutByID return a TabsLayout with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not TabsLayout
|
||||||
|
func TabsLayoutByID(rootView View, id string) TabsLayout {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if list, ok := view.(TabsLayout); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
ErrorLog(`TabsLayoutByID(_, "` + id + `"): The found View is not TabsLayout`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListViewByID return a ListView with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not ListView
|
||||||
|
func ListViewByID(rootView View, id string) ListView {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if list, ok := view.(ListView); ok {
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
ErrorLog(`ListViewByID(_, "` + id + `"): The found View is not ListView`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TextViewByID return a TextView with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not TextView
|
||||||
|
func TextViewByID(rootView View, id string) TextView {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if text, ok := view.(TextView); ok {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
ErrorLog(`TextViewByID(_, "` + id + `"): The found View is not TextView`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ButtonByID return a Button with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not Button
|
||||||
|
func ButtonByID(rootView View, id string) Button {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if button, ok := view.(Button); ok {
|
||||||
|
return button
|
||||||
|
}
|
||||||
|
ErrorLog(`ButtonByID(_, "` + id + `"): The found View is not Button`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckboxByID return a Checkbox with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not Checkbox
|
||||||
|
func CheckboxByID(rootView View, id string) Checkbox {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if checkbox, ok := view.(Checkbox); ok {
|
||||||
|
return checkbox
|
||||||
|
}
|
||||||
|
ErrorLog(`CheckboxByID(_, "` + id + `"): The found View is not Checkbox`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditViewByID return a EditView with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not EditView
|
||||||
|
func EditViewByID(rootView View, id string) EditView {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if buttons, ok := view.(EditView); ok {
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
ErrorLog(`EditViewByID(_, "` + id + `"): The found View is not EditView`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgressBarByID return a ProgressBar with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not ProgressBar
|
||||||
|
func ProgressBarByID(rootView View, id string) ProgressBar {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if buttons, ok := view.(ProgressBar); ok {
|
||||||
|
return buttons
|
||||||
|
}
|
||||||
|
ErrorLog(`ProgressBarByID(_, "` + id + `"): The found View is not ProgressBar`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumberPickerByID return a NumberPicker with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not NumberPicker
|
||||||
|
func NumberPickerByID(rootView View, id string) NumberPicker {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if input, ok := view.(NumberPicker); ok {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
ErrorLog(`NumberPickerByID(_, "` + id + `"): The found View is not NumberPicker`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanvasViewByID return a CanvasView with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not CanvasView
|
||||||
|
func CanvasViewByID(rootView View, id string) CanvasView {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if canvas, ok := view.(CanvasView); ok {
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
ErrorLog(`CanvasViewByID(_, "` + id + `"): The found View is not CanvasView`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TableViewByID return a TableView with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not TableView
|
||||||
|
func TableViewByID(rootView View, id string) TableView {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if canvas, ok := view.(TableView); ok {
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
ErrorLog(`TableViewByID(_, "` + id + `"): The found View is not TableView`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// AudioPlayerByID return a AudioPlayer with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not AudioPlayer
|
||||||
|
func AudioPlayerByID(rootView View, id string) AudioPlayer {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if canvas, ok := view.(AudioPlayer); ok {
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
ErrorLog(`AudioPlayerByID(_, "` + id + `"): The found View is not AudioPlayer`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VideoPlayerByID return a VideoPlayer with id equal to the argument of the function or
|
||||||
|
// nil if there is no such View or View is not VideoPlayer
|
||||||
|
func VideoPlayerByID(rootView View, id string) VideoPlayer {
|
||||||
|
if view := ViewByID(rootView, id); view != nil {
|
||||||
|
if canvas, ok := view.(VideoPlayer); ok {
|
||||||
|
return canvas
|
||||||
|
}
|
||||||
|
ErrorLog(`VideoPlayerByID(_, "` + id + `"): The found View is not VideoPlayer`)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,594 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClipShape defines a View clipping area
|
||||||
|
type ClipShape interface {
|
||||||
|
Properties
|
||||||
|
fmt.Stringer
|
||||||
|
ruiStringer
|
||||||
|
cssStyle(session Session) string
|
||||||
|
valid(session Session) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type insetClip struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
type ellipseClip struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
type polygonClip struct {
|
||||||
|
points []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsetClip creates a rectangle View clipping area.
|
||||||
|
// top - offset from the top border of a View;
|
||||||
|
// right - offset from the right border of a View;
|
||||||
|
// bottom - offset from the bottom border of a View;
|
||||||
|
// left - offset from the left border of a View;
|
||||||
|
// radius - corner radius, pass nil if you don't need to round corners
|
||||||
|
func InsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShape {
|
||||||
|
clip := new(insetClip)
|
||||||
|
clip.init()
|
||||||
|
clip.Set(Top, top)
|
||||||
|
clip.Set(Right, right)
|
||||||
|
clip.Set(Bottom, bottom)
|
||||||
|
clip.Set(Left, left)
|
||||||
|
if radius != nil {
|
||||||
|
clip.Set(Radius, radius)
|
||||||
|
}
|
||||||
|
return clip
|
||||||
|
}
|
||||||
|
|
||||||
|
// CircleClip creates a circle View clipping area.
|
||||||
|
func CircleClip(x, y, radius SizeUnit) ClipShape {
|
||||||
|
clip := new(ellipseClip)
|
||||||
|
clip.init()
|
||||||
|
clip.Set(X, x)
|
||||||
|
clip.Set(Y, y)
|
||||||
|
clip.Set(Radius, radius)
|
||||||
|
return clip
|
||||||
|
}
|
||||||
|
|
||||||
|
// EllipseClip creates a ellipse View clipping area.
|
||||||
|
func EllipseClip(x, y, rx, ry SizeUnit) ClipShape {
|
||||||
|
clip := new(ellipseClip)
|
||||||
|
clip.init()
|
||||||
|
clip.Set(X, x)
|
||||||
|
clip.Set(Y, y)
|
||||||
|
clip.Set(RadiusX, rx)
|
||||||
|
clip.Set(RadiusY, ry)
|
||||||
|
return clip
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolygonClip creates a polygon View clipping area.
|
||||||
|
// The elements of the function argument can be or text constants,
|
||||||
|
// or the text representation of SizeUnit, or elements of SizeUnit type.
|
||||||
|
func PolygonClip(points []interface{}) ClipShape {
|
||||||
|
clip := new(polygonClip)
|
||||||
|
clip.points = []interface{}{}
|
||||||
|
if clip.Set(Points, points) {
|
||||||
|
return clip
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PolygonPointsClip creates a polygon View clipping area.
|
||||||
|
func PolygonPointsClip(points []SizeUnit) ClipShape {
|
||||||
|
clip := new(polygonClip)
|
||||||
|
clip.points = []interface{}{}
|
||||||
|
if clip.Set(Points, points) {
|
||||||
|
return clip
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *insetClip) Set(tag string, value interface{}) bool {
|
||||||
|
switch strings.ToLower(tag) {
|
||||||
|
case Top, Right, Bottom, Left:
|
||||||
|
if value == nil {
|
||||||
|
clip.Remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return clip.setSizeProperty(tag, value)
|
||||||
|
|
||||||
|
case Radius:
|
||||||
|
return clip.setRadius(value)
|
||||||
|
|
||||||
|
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
|
||||||
|
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
|
||||||
|
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
|
||||||
|
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
|
||||||
|
return clip.setRadiusElement(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLogF(`"%s" property is not supported by the inset clip shape`, tag)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *insetClip) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
clip.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *insetClip) ruiString(writer ruiWriter) {
|
||||||
|
writer.startObject("inset")
|
||||||
|
for _, tag := range []string{Top, Right, Bottom, Left} {
|
||||||
|
if value, ok := clip.properties[tag]; ok {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
writer.writeProperty(tag, value)
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
writer.writeProperty(tag, value.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := clip.Get(Radius); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case RadiusProperty:
|
||||||
|
writer.writeProperty(Radius, value.String())
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
writer.writeProperty(Radius, value.String())
|
||||||
|
|
||||||
|
case string:
|
||||||
|
writer.writeProperty(Radius, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *insetClip) cssStyle(session Session) string {
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
leadText := "inset("
|
||||||
|
for _, tag := range []string{Top, Right, Bottom, Left} {
|
||||||
|
value, _ := sizeProperty(clip, tag, session)
|
||||||
|
buffer.WriteString(leadText)
|
||||||
|
buffer.WriteString(value.cssString("0px"))
|
||||||
|
leadText = " "
|
||||||
|
}
|
||||||
|
|
||||||
|
if radius := getRadiusProperty(clip); radius != nil {
|
||||||
|
buffer.WriteString(" round ")
|
||||||
|
buffer.WriteString(radius.BoxRadius(session).cssString())
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *insetClip) valid(session Session) bool {
|
||||||
|
for _, tag := range []string{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
|
||||||
|
if value, ok := sizeProperty(clip, tag, session); ok && value.Type != Auto && value.Value != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *ellipseClip) Set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
clip.Remove(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(tag) {
|
||||||
|
case X, Y:
|
||||||
|
return clip.setSizeProperty(tag, value)
|
||||||
|
|
||||||
|
case Radius:
|
||||||
|
result := clip.setSizeProperty(tag, value)
|
||||||
|
if result {
|
||||||
|
delete(clip.properties, RadiusX)
|
||||||
|
delete(clip.properties, RadiusY)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
case RadiusX:
|
||||||
|
result := clip.setSizeProperty(tag, value)
|
||||||
|
if result {
|
||||||
|
if r, ok := clip.properties[Radius]; ok {
|
||||||
|
clip.properties[RadiusY] = r
|
||||||
|
delete(clip.properties, Radius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
case RadiusY:
|
||||||
|
result := clip.setSizeProperty(tag, value)
|
||||||
|
if result {
|
||||||
|
if r, ok := clip.properties[Radius]; ok {
|
||||||
|
clip.properties[RadiusX] = r
|
||||||
|
delete(clip.properties, Radius)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLogF(`"%s" property is not supported by the inset clip shape`, tag)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *ellipseClip) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
clip.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *ellipseClip) ruiString(writer ruiWriter) {
|
||||||
|
writeProperty := func(tag string, value interface{}) {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
writer.writeProperty(tag, value)
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
writer.writeProperty(tag, value.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, ok := clip.properties[Radius]; ok {
|
||||||
|
writer.startObject("circle")
|
||||||
|
writeProperty(Radius, r)
|
||||||
|
} else {
|
||||||
|
writer.startObject("ellipse")
|
||||||
|
for _, tag := range []string{RadiusX, RadiusY} {
|
||||||
|
if value, ok := clip.properties[tag]; ok {
|
||||||
|
writeProperty(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range []string{X, Y} {
|
||||||
|
if value, ok := clip.properties[tag]; ok {
|
||||||
|
writeProperty(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *ellipseClip) cssStyle(session Session) string {
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
if r, ok := sizeProperty(clip, Radius, session); ok {
|
||||||
|
buffer.WriteString("circle(")
|
||||||
|
buffer.WriteString(r.cssString("0"))
|
||||||
|
} else {
|
||||||
|
rx, _ := sizeProperty(clip, RadiusX, session)
|
||||||
|
ry, _ := sizeProperty(clip, RadiusX, session)
|
||||||
|
buffer.WriteString("ellipse(")
|
||||||
|
buffer.WriteString(rx.cssString("0"))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
buffer.WriteString(ry.cssString("0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString(" at ")
|
||||||
|
x, _ := sizeProperty(clip, X, session)
|
||||||
|
buffer.WriteString(x.cssString("0"))
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
|
||||||
|
y, _ := sizeProperty(clip, Y, session)
|
||||||
|
buffer.WriteString(y.cssString("0"))
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *ellipseClip) valid(session Session) bool {
|
||||||
|
if value, ok := sizeProperty(clip, Radius, session); ok && value.Type != Auto && value.Value != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
rx, okX := sizeProperty(clip, RadiusX, session)
|
||||||
|
ry, okY := sizeProperty(clip, RadiusY, session)
|
||||||
|
return okX && okY && rx.Type != Auto && rx.Value != 0 && ry.Type != Auto && ry.Value != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) Get(tag string) interface{} {
|
||||||
|
if Points == strings.ToLower(tag) {
|
||||||
|
return clip.points
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) getRaw(tag string) interface{} {
|
||||||
|
return clip.Get(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) Set(tag string, value interface{}) bool {
|
||||||
|
if Points == strings.ToLower(tag) {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
result := true
|
||||||
|
clip.points = make([]interface{}, len(value))
|
||||||
|
for i, val := range value {
|
||||||
|
switch val := val.(type) {
|
||||||
|
case string:
|
||||||
|
if isConstantName(val) {
|
||||||
|
clip.points[i] = val
|
||||||
|
} else if size, ok := StringToSizeUnit(val); ok {
|
||||||
|
clip.points[i] = size
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
clip.points[i] = val
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
clip.points[i] = AutoSize()
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
case []SizeUnit:
|
||||||
|
clip.points = make([]interface{}, len(value))
|
||||||
|
for i, point := range value {
|
||||||
|
clip.points[i] = point
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
result := true
|
||||||
|
values := strings.Split(value, ",")
|
||||||
|
clip.points = make([]interface{}, len(values))
|
||||||
|
for i, val := range values {
|
||||||
|
val = strings.Trim(val, " \t\n\r")
|
||||||
|
if isConstantName(val) {
|
||||||
|
clip.points[i] = val
|
||||||
|
} else if size, ok := StringToSizeUnit(val); ok {
|
||||||
|
clip.points[i] = size
|
||||||
|
} else {
|
||||||
|
notCompatibleType(tag, val)
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) setRaw(tag string, value interface{}) {
|
||||||
|
clip.Set(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) Remove(tag string) {
|
||||||
|
if Points == strings.ToLower(tag) {
|
||||||
|
clip.points = []interface{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) Clear() {
|
||||||
|
clip.points = []interface{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) AllTags() []string {
|
||||||
|
return []string{Points}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
clip.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) ruiString(writer ruiWriter) {
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
writer.startObject("polygon")
|
||||||
|
|
||||||
|
if clip.points != nil {
|
||||||
|
for i, value := range clip.points {
|
||||||
|
if i > 0 {
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
}
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
buffer.WriteString(value)
|
||||||
|
|
||||||
|
case fmt.Stringer:
|
||||||
|
buffer.WriteString(value.String())
|
||||||
|
|
||||||
|
default:
|
||||||
|
buffer.WriteString("0px")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.writeProperty(Points, buffer.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) cssStyle(session Session) string {
|
||||||
|
|
||||||
|
if clip.points == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(clip.points)
|
||||||
|
if count < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
writePoint := func(value interface{}) {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if val, ok := session.resolveConstants(value); ok {
|
||||||
|
if size, ok := StringToSizeUnit(val); ok {
|
||||||
|
buffer.WriteString(size.cssString("0px"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case SizeUnit:
|
||||||
|
buffer.WriteString(value.cssString("0px"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteString("0px")
|
||||||
|
}
|
||||||
|
|
||||||
|
leadText := "polygon("
|
||||||
|
for i := 1; i < count; i += 2 {
|
||||||
|
buffer.WriteString(leadText)
|
||||||
|
writePoint(clip.points[i-1])
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
writePoint(clip.points[i])
|
||||||
|
leadText = ", "
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (clip *polygonClip) valid(session Session) bool {
|
||||||
|
if clip.points == nil || len(clip.points) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseClipShape(obj DataObject) ClipShape {
|
||||||
|
switch obj.Tag() {
|
||||||
|
case "inset":
|
||||||
|
clip := new(insetClip)
|
||||||
|
for _, tag := range []string{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
|
||||||
|
if value, ok := obj.PropertyValue(tag); ok {
|
||||||
|
clip.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clip
|
||||||
|
|
||||||
|
case "circle":
|
||||||
|
clip := new(ellipseClip)
|
||||||
|
for _, tag := range []string{X, Y, Radius} {
|
||||||
|
if value, ok := obj.PropertyValue(tag); ok {
|
||||||
|
clip.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clip
|
||||||
|
|
||||||
|
case "ellipse":
|
||||||
|
clip := new(ellipseClip)
|
||||||
|
for _, tag := range []string{X, Y, RadiusX, RadiusY} {
|
||||||
|
if value, ok := obj.PropertyValue(tag); ok {
|
||||||
|
clip.Set(tag, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clip
|
||||||
|
|
||||||
|
case "polygon":
|
||||||
|
clip := new(ellipseClip)
|
||||||
|
if value, ok := obj.PropertyValue(Points); ok {
|
||||||
|
clip.Set(Points, value)
|
||||||
|
}
|
||||||
|
return clip
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) setClipShape(tag string, value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case ClipShape:
|
||||||
|
style.properties[tag] = value
|
||||||
|
return true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if isConstantName(value) {
|
||||||
|
style.properties[tag] = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj := NewDataObject(value); obj == nil {
|
||||||
|
if clip := parseClipShape(obj); clip != nil {
|
||||||
|
style.properties[tag] = clip
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
if clip := parseClipShape(value); clip != nil {
|
||||||
|
style.properties[tag] = clip
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataValue:
|
||||||
|
if value.IsObject() {
|
||||||
|
if clip := parseClipShape(value.Object()); clip != nil {
|
||||||
|
style.properties[tag] = clip
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClipShape(prop Properties, tag string, session Session) ClipShape {
|
||||||
|
if value := prop.getRaw(tag); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case ClipShape:
|
||||||
|
return value
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if text, ok := session.resolveConstants(value); ok {
|
||||||
|
if obj := NewDataObject(text); obj == nil {
|
||||||
|
return parseClipShape(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClip returns a View clipping area.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetClip(view View, subviewID string) ClipShape {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
return getClipShape(view, Clip, view.Session())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetShapeOutside returns a shape around which adjacent inline content.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetShapeOutside(view View, subviewID string) ClipShape {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
return getClipShape(view, ShapeOutside, view.Session())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var viewCreators = map[string]func(Session) View{
|
||||||
|
"View": newView,
|
||||||
|
"ColumnLayout": newColumnLayout,
|
||||||
|
"ListLayout": newListLayout,
|
||||||
|
"GridLayout": newGridLayout,
|
||||||
|
"StackLayout": newStackLayout,
|
||||||
|
"TabsLayout": newTabsLayout,
|
||||||
|
"AbsoluteLayout": newAbsoluteLayout,
|
||||||
|
"Resizable": newResizable,
|
||||||
|
"DetailsView": newDetailsView,
|
||||||
|
"TextView": newTextView,
|
||||||
|
"Button": newButton,
|
||||||
|
"Checkbox": newCheckbox,
|
||||||
|
"DropDownList": newDropDownList,
|
||||||
|
"ProgressBar": newProgressBar,
|
||||||
|
"NumberPicker": newNumberPicker,
|
||||||
|
"ColorPicker": newColorPicker,
|
||||||
|
"DatePicker": newDatePicker,
|
||||||
|
"TimePicker": newTimePicker,
|
||||||
|
"EditView": newEditView,
|
||||||
|
"ListView": newListView,
|
||||||
|
"CanvasView": newCanvasView,
|
||||||
|
"ImageView": newImageView,
|
||||||
|
"TableView": newTableView,
|
||||||
|
"AudioPlayer": newAudioPlayer,
|
||||||
|
"VideoPlayer": newVideoPlayer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterViewCreator register function of creating view
|
||||||
|
func RegisterViewCreator(tag string, creator func(Session) View) bool {
|
||||||
|
builtinViews := []string{
|
||||||
|
"View",
|
||||||
|
"ViewsContainer",
|
||||||
|
"ColumnLayout",
|
||||||
|
"ListLayout",
|
||||||
|
"GridLayout",
|
||||||
|
"StackLayout",
|
||||||
|
"TabsLayout",
|
||||||
|
"AbsoluteLayout",
|
||||||
|
"Resizable",
|
||||||
|
"DetailsView",
|
||||||
|
"TextView",
|
||||||
|
"Button",
|
||||||
|
"Checkbox",
|
||||||
|
"DropDownList",
|
||||||
|
"ProgressBar",
|
||||||
|
"NumberPicker",
|
||||||
|
"ColorPicker",
|
||||||
|
"DatePicker",
|
||||||
|
"TimePicker",
|
||||||
|
"EditView",
|
||||||
|
"ListView",
|
||||||
|
"CanvasView",
|
||||||
|
"ImageView",
|
||||||
|
"TableView",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range builtinViews {
|
||||||
|
if name == tag {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewCreators[tag] = creator
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateViewFromObject create new View and initialize it by Node data
|
||||||
|
func CreateViewFromObject(session Session, object DataObject) View {
|
||||||
|
tag := object.Tag()
|
||||||
|
|
||||||
|
if creator, ok := viewCreators[tag]; ok {
|
||||||
|
if !session.ignoreViewUpdates() {
|
||||||
|
session.setIgnoreViewUpdates(true)
|
||||||
|
defer session.setIgnoreViewUpdates(false)
|
||||||
|
}
|
||||||
|
view := creator(session)
|
||||||
|
if customView, ok := view.(CustomView); ok {
|
||||||
|
if !InitCustomView(customView, tag, session, nil) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseProperties(view, object)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLog(`Unknown view type "` + object.Tag() + `"`)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateViewFromText create new View and initialize it by content of text
|
||||||
|
func CreateViewFromText(session Session, text string) View {
|
||||||
|
if data := ParseDataText(text); data != nil {
|
||||||
|
return CreateViewFromObject(session, data)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateViewFromResources create new View and initialize it by the content of
|
||||||
|
// the resource file from "views" directory
|
||||||
|
func CreateViewFromResources(session Session, name string) View {
|
||||||
|
if strings.ToLower(filepath.Ext(name)) != ".rui" {
|
||||||
|
name += ".rui"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fs := range resources.embedFS {
|
||||||
|
rootDirs := embedRootDirs(fs)
|
||||||
|
for _, dir := range rootDirs {
|
||||||
|
switch dir {
|
||||||
|
case imageDir, themeDir, rawDir:
|
||||||
|
// do nothing
|
||||||
|
|
||||||
|
case viewDir:
|
||||||
|
if data, err := fs.ReadFile(dir + "/" + name); err == nil {
|
||||||
|
if data := ParseDataText(string(data)); data != nil {
|
||||||
|
return CreateViewFromObject(session, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if data, err := fs.ReadFile(dir + "/" + viewDir + "/" + name); err == nil {
|
||||||
|
if data := ParseDataText(string(data)); data != nil {
|
||||||
|
return CreateViewFromObject(session, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,264 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Blur is the constant for the "blur" property tag of the ViewFilter interface.
|
||||||
|
// The "blur" float64 property applies a Gaussian blur. The value of radius defines the value
|
||||||
|
// of the standard deviation to the Gaussian function, or how many pixels on the screen blend
|
||||||
|
// into each other, so a larger value will create more blur. The lacuna value for interpolation is 0.
|
||||||
|
// The parameter is specified as a length in pixels.
|
||||||
|
Blur = "blur"
|
||||||
|
|
||||||
|
// Brightness is the constant for the "brightness" property tag of the ViewFilter interface.
|
||||||
|
// The "brightness" float64 property applies a linear multiplier to input image, making it appear more
|
||||||
|
// or less bright. A value of 0% will create an image that is completely black.
|
||||||
|
// A value of 100% leaves the input unchanged. Other values are linear multipliers on the effect.
|
||||||
|
// Values of an amount over 100% are allowed, providing brighter results.
|
||||||
|
Brightness = "brightness"
|
||||||
|
|
||||||
|
// Contrast is the constant for the "contrast" property tag of the ViewFilter interface.
|
||||||
|
// The "contrast" float64 property adjusts the contrast of the input.
|
||||||
|
// A value of 0% will create an image that is completely black. A value of 100% leaves the input unchanged.
|
||||||
|
// Values of amount over 100% are allowed, providing results with less contrast.
|
||||||
|
Contrast = "contrast"
|
||||||
|
|
||||||
|
// DropShadow is the constant for the "drop-shadow" property tag of the ViewFilter interface.
|
||||||
|
// The "drop-shadow" property applies a drop shadow effect to the input image.
|
||||||
|
// A drop shadow is effectively a blurred, offset version of the input image's alpha mask
|
||||||
|
// drawn in a particular color, composited below the image.
|
||||||
|
// Shadow parameters are set using the ViewShadow interface
|
||||||
|
DropShadow = "drop-shadow"
|
||||||
|
|
||||||
|
// Grayscale is the constant for the "grayscale" property tag of the ViewFilter interface.
|
||||||
|
// The "grayscale" float64 property converts the input image to grayscale.
|
||||||
|
// The value of ‘amount’ defines the proportion of the conversion.
|
||||||
|
// A value of 100% is completely grayscale. A value of 0% leaves the input unchanged.
|
||||||
|
// Values between 0% and 100% are linear multipliers on the effect.
|
||||||
|
Grayscale = "grayscale"
|
||||||
|
|
||||||
|
// HueRotate is the constant for the "hue-rotate" property tag of the ViewFilter interface.
|
||||||
|
// The "hue-rotate" AngleUnit property applies a hue rotation on the input image.
|
||||||
|
// The value of ‘angle’ defines the number of degrees around the color circle the input samples will be adjusted.
|
||||||
|
// A value of 0deg leaves the input unchanged. If the ‘angle’ parameter is missing, a value of 0deg is used.
|
||||||
|
// Though there is no maximum value, the effect of values above 360deg wraps around.
|
||||||
|
HueRotate = "hue-rotate"
|
||||||
|
|
||||||
|
// Invert is the constant for the "invert" property tag of the ViewFilter interface.
|
||||||
|
// The "invert" float64 property inverts the samples in the input image.
|
||||||
|
// The value of ‘amount’ defines the proportion of the conversion.
|
||||||
|
// A value of 100% is completely inverted. A value of 0% leaves the input unchanged.
|
||||||
|
// Values between 0% and 100% are linear multipliers on the effect.
|
||||||
|
Invert = "invert"
|
||||||
|
|
||||||
|
// Saturate is the constant for the "saturate" property tag of the ViewFilter interface.
|
||||||
|
// The "saturate" float64 property saturates the input image.
|
||||||
|
// The value of ‘amount’ defines the proportion of the conversion.
|
||||||
|
// A value of 0% is completely un-saturated. A value of 100% leaves the input unchanged.
|
||||||
|
// Other values are linear multipliers on the effect.
|
||||||
|
// Values of amount over 100% are allowed, providing super-saturated results.
|
||||||
|
Saturate = "saturate"
|
||||||
|
|
||||||
|
// Sepia is the constant for the "sepia" property tag of the ViewFilter interface.
|
||||||
|
// The "sepia" float64 property converts the input image to sepia.
|
||||||
|
// The value of ‘amount’ defines the proportion of the conversion.
|
||||||
|
// A value of 100% is completely sepia. A value of 0% leaves the input unchanged.
|
||||||
|
// Values between 0% and 100% are linear multipliers on the effect.
|
||||||
|
Sepia = "sepia"
|
||||||
|
|
||||||
|
//Opacity = "opacity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ViewFilter defines an applied to a View a graphical effects like blur or color shift.
|
||||||
|
// Allowable properties are Blur, Brightness, Contrast, DropShadow, Grayscale, HueRotate, Invert, Opacity, Saturate, and Sepia
|
||||||
|
type ViewFilter interface {
|
||||||
|
Properties
|
||||||
|
fmt.Stringer
|
||||||
|
ruiStringer
|
||||||
|
cssStyle(session Session) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type viewFilter struct {
|
||||||
|
propertyList
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewViewFilter creates the new ViewFilter
|
||||||
|
func NewViewFilter(params Params) ViewFilter {
|
||||||
|
filter := new(viewFilter)
|
||||||
|
filter.init()
|
||||||
|
for tag, value := range params {
|
||||||
|
filter.Set(tag, value)
|
||||||
|
}
|
||||||
|
if len(filter.properties) > 0 {
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newViewFilter(obj DataObject) ViewFilter {
|
||||||
|
filter := new(viewFilter)
|
||||||
|
filter.init()
|
||||||
|
for i := 0; i < obj.PropertyCount(); i++ {
|
||||||
|
if node := obj.Property(i); node != nil {
|
||||||
|
tag := node.Tag()
|
||||||
|
switch node.Type() {
|
||||||
|
case TextNode:
|
||||||
|
filter.Set(tag, node.Text())
|
||||||
|
|
||||||
|
case ObjectNode:
|
||||||
|
if tag == HueRotate {
|
||||||
|
// TODO
|
||||||
|
} else {
|
||||||
|
ErrorLog(`Invalid value of "` + tag + `"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
ErrorLog(`Invalid value of "` + tag + `"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.properties) > 0 {
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
ErrorLog("Empty view filter")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filter *viewFilter) Set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
filter.Remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(tag) {
|
||||||
|
case Blur, Brightness, Contrast, Saturate:
|
||||||
|
return filter.setFloatProperty(tag, value, 0, 10000)
|
||||||
|
|
||||||
|
case Grayscale, Invert, Opacity, Sepia:
|
||||||
|
return filter.setFloatProperty(tag, value, 0, 100)
|
||||||
|
|
||||||
|
case HueRotate:
|
||||||
|
return filter.setAngleProperty(tag, value)
|
||||||
|
|
||||||
|
case DropShadow:
|
||||||
|
return filter.setShadow(tag, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorLogF(`"%s" property is not supported by the view filter`, tag)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filter *viewFilter) String() string {
|
||||||
|
writer := newRUIWriter()
|
||||||
|
filter.ruiString(writer)
|
||||||
|
return writer.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filter *viewFilter) ruiString(writer ruiWriter) {
|
||||||
|
writer.startObject("filter")
|
||||||
|
for tag, value := range filter.properties {
|
||||||
|
writer.writeProperty(tag, value)
|
||||||
|
}
|
||||||
|
writer.endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (filter *viewFilter) cssStyle(session Session) string {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
if value, ok := floatProperty(filter, Blur, session, 0); ok {
|
||||||
|
size := SizeUnit{Type: SizeInPixel, Value: value}
|
||||||
|
buffer.WriteString(Blur)
|
||||||
|
buffer.WriteRune('(')
|
||||||
|
buffer.WriteString(size.cssString("0px"))
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range []string{Brightness, Contrast, Saturate, Grayscale, Invert, Opacity, Sepia} {
|
||||||
|
if value, ok := floatProperty(filter, tag, session, 0); ok {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(fmt.Sprintf("%s(%g%%)", tag, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := angleProperty(filter, HueRotate, session); ok {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(HueRotate)
|
||||||
|
buffer.WriteRune('(')
|
||||||
|
buffer.WriteString(value.cssString())
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
var lead string
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
lead = " drop-shadow("
|
||||||
|
} else {
|
||||||
|
lead = "drop-shadow("
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, shadow := range getShadows(filter, DropShadow) {
|
||||||
|
if shadow.cssTextStyle(buffer, session, lead) {
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
lead = " drop-shadow("
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) setFilter(value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case ViewFilter:
|
||||||
|
style.properties[Filter] = value
|
||||||
|
return true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if obj := NewDataObject(value); obj == nil {
|
||||||
|
if filter := newViewFilter(obj); filter != nil {
|
||||||
|
style.properties[Filter] = filter
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case DataObject:
|
||||||
|
if filter := newViewFilter(value); filter != nil {
|
||||||
|
style.properties[Filter] = filter
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case DataValue:
|
||||||
|
if value.IsObject() {
|
||||||
|
if filter := newViewFilter(value.Object()); filter != nil {
|
||||||
|
style.properties[Filter] = filter
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notCompatibleType(Filter, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilter returns a View graphical effects like blur or color shift.
|
||||||
|
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
|
||||||
|
func GetFilter(view View, subviewID string) ViewFilter {
|
||||||
|
if subviewID != "" {
|
||||||
|
view = ViewByID(view, subviewID)
|
||||||
|
}
|
||||||
|
if view != nil {
|
||||||
|
if value := view.getRaw(Filter); value != nil {
|
||||||
|
if filter, ok := value.(ViewFilter); ok {
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,422 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ViewStyle interface of the style of view
|
||||||
|
type ViewStyle interface {
|
||||||
|
Properties
|
||||||
|
cssViewStyle(buffer cssBuilder, session Session, view View)
|
||||||
|
}
|
||||||
|
|
||||||
|
type viewStyle struct {
|
||||||
|
propertyList
|
||||||
|
//transitions map[string]ViewTransition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range defines range limits. The First and Last value are included in the range
|
||||||
|
type Range struct {
|
||||||
|
First, Last int
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the Range struct
|
||||||
|
func (r Range) String() string {
|
||||||
|
if r.First == r.Last {
|
||||||
|
return fmt.Sprintf("%d", r.First)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d:%d", r.First, r.Last)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Range) setValue(value string) bool {
|
||||||
|
var err error
|
||||||
|
if strings.Contains(value, ":") {
|
||||||
|
values := strings.Split(value, ":")
|
||||||
|
if len(values) != 2 {
|
||||||
|
ErrorLog("Invalid range value: " + value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.First, err = strconv.Atoi(strings.Trim(values[0], " \t\n\r")); err != nil {
|
||||||
|
ErrorLog(`Invalid first range value "` + value + `" (` + err.Error() + ")")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if r.Last, err = strconv.Atoi(strings.Trim(values[1], " \t\n\r")); err != nil {
|
||||||
|
ErrorLog(`Invalid last range value "` + value + `" (` + err.Error() + ")")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.First, err = strconv.Atoi(value); err != nil {
|
||||||
|
ErrorLog(`Invalid range value "` + value + `" (` + err.Error() + ")")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
r.Last = r.First
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) init() {
|
||||||
|
style.propertyList.init()
|
||||||
|
//style.shadows = []ViewShadow{}
|
||||||
|
//style.transitions = map[string]ViewTransition{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewViewStyle create new ViewStyle object
|
||||||
|
func NewViewStyle(params Params) ViewStyle {
|
||||||
|
style := new(viewStyle)
|
||||||
|
style.init()
|
||||||
|
for tag, value := range params {
|
||||||
|
style.Set(tag, value)
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) cssTextDecoration(session Session) string {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
noDecoration := false
|
||||||
|
if strikethrough, ok := boolProperty(style, Strikethrough, session); ok {
|
||||||
|
if strikethrough {
|
||||||
|
buffer.WriteString("line-through")
|
||||||
|
}
|
||||||
|
noDecoration = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if overline, ok := boolProperty(style, Overline, session); ok {
|
||||||
|
if overline {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString("overline")
|
||||||
|
}
|
||||||
|
noDecoration = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if underline, ok := boolProperty(style, Underline, session); ok {
|
||||||
|
if underline {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString("underline")
|
||||||
|
}
|
||||||
|
noDecoration = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer.Len() == 0 && noDecoration {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func split4Values(text string) []string {
|
||||||
|
values := strings.Split(text, ",")
|
||||||
|
count := len(values)
|
||||||
|
switch count {
|
||||||
|
case 1, 4:
|
||||||
|
return values
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
if strings.Trim(values[1], " \t\r\n") == "" {
|
||||||
|
return values[:1]
|
||||||
|
}
|
||||||
|
|
||||||
|
case 5:
|
||||||
|
if strings.Trim(values[4], " \t\r\n") != "" {
|
||||||
|
return values[:4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) backgroundCSS(view View) string {
|
||||||
|
if value, ok := style.properties[Background]; ok {
|
||||||
|
if backgrounds, ok := value.([]BackgroundElement); ok {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
for _, background := range backgrounds {
|
||||||
|
if value := background.cssStyle(view); value != "" {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
}
|
||||||
|
buffer.WriteString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session, view View) {
|
||||||
|
|
||||||
|
if margin, ok := boundsProperty(style, Margin, session); ok {
|
||||||
|
margin.cssValue(Margin, builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if padding, ok := boundsProperty(style, Padding, session); ok {
|
||||||
|
padding.cssValue(Padding, builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if border := getBorder(style, Border); border != nil {
|
||||||
|
border.cssStyle(builder, session)
|
||||||
|
border.cssWidth(builder, session)
|
||||||
|
border.cssColor(builder, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
radius := getRadius(style, session)
|
||||||
|
radius.cssValue(builder)
|
||||||
|
|
||||||
|
if outline := getOutline(style); outline != nil {
|
||||||
|
outline.ViewOutline(session).cssValue(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if z, ok := intProperty(style, ZIndex, session, 0); ok {
|
||||||
|
builder.add(ZIndex, strconv.Itoa(z))
|
||||||
|
}
|
||||||
|
|
||||||
|
if opacity, ok := floatProperty(style, Opacity, session, 1.0); ok && opacity >= 0 && opacity <= 1 {
|
||||||
|
builder.add(Opacity, strconv.FormatFloat(opacity, 'f', 3, 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, ok := intProperty(style, ColumnCount, session, 0); ok && n > 0 {
|
||||||
|
builder.add(ColumnCount, strconv.Itoa(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range []string{
|
||||||
|
Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Left, Right, Top, Bottom,
|
||||||
|
TextSize, TextIndent, LetterSpacing, WordSpacing, LineHeight, TextLineThickness,
|
||||||
|
GridRowGap, GridColumnGap, ColumnGap, ColumnWidth} {
|
||||||
|
|
||||||
|
if size, ok := sizeProperty(style, tag, session); ok && size.Type != Auto {
|
||||||
|
cssTag, ok := sizeProperties[tag]
|
||||||
|
if !ok {
|
||||||
|
cssTag = tag
|
||||||
|
}
|
||||||
|
builder.add(cssTag, size.cssString(""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
colorProperties := []struct{ property, cssTag string }{
|
||||||
|
{BackgroundColor, BackgroundColor},
|
||||||
|
{TextColor, "color"},
|
||||||
|
{TextLineColor, "text-decoration-color"},
|
||||||
|
}
|
||||||
|
for _, p := range colorProperties {
|
||||||
|
if color, ok := colorProperty(style, p.property, session); ok && color != 0 {
|
||||||
|
builder.add(p.cssTag, color.cssString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := enumProperty(style, BackgroundClip, session, 0); ok {
|
||||||
|
builder.add(BackgroundClip, enumProperties[BackgroundClip].values[value])
|
||||||
|
}
|
||||||
|
|
||||||
|
if background := style.backgroundCSS(view); background != "" {
|
||||||
|
builder.add("background", background)
|
||||||
|
}
|
||||||
|
|
||||||
|
if font, ok := stringProperty(style, FontName, session); ok && font != "" {
|
||||||
|
builder.add(`font-family`, font)
|
||||||
|
}
|
||||||
|
|
||||||
|
writingMode := 0
|
||||||
|
for _, tag := range []string{
|
||||||
|
TextAlign, TextTransform, TextWeight, TextLineStyle, WritingMode, TextDirection,
|
||||||
|
VerticalTextOrientation, CellVerticalAlign, CellHorizontalAlign, Cursor, WhiteSpace,
|
||||||
|
WordBreak, TextOverflow, Float, TableVerticalAlign} {
|
||||||
|
|
||||||
|
if data, ok := enumProperties[tag]; ok {
|
||||||
|
if tag != VerticalTextOrientation || (writingMode != VerticalLeftToRight && writingMode != VerticalRightToLeft) {
|
||||||
|
if value, ok := enumProperty(style, tag, session, 0); ok {
|
||||||
|
cssValue := data.values[value]
|
||||||
|
if cssValue != "" {
|
||||||
|
builder.add(data.cssTag, cssValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag == WritingMode {
|
||||||
|
writingMode = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prop := range []struct{ tag, cssTag, off, on string }{
|
||||||
|
{tag: Italic, cssTag: "font-style", off: "normal", on: "italic"},
|
||||||
|
{tag: SmallCaps, cssTag: "font-variant", off: "normal", on: "small-caps"},
|
||||||
|
} {
|
||||||
|
if flag, ok := boolProperty(style, prop.tag, session); ok {
|
||||||
|
if flag {
|
||||||
|
builder.add(prop.cssTag, prop.on)
|
||||||
|
} else {
|
||||||
|
builder.add(prop.cssTag, prop.off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if text := style.cssTextDecoration(session); text != "" {
|
||||||
|
builder.add("text-decoration", text)
|
||||||
|
}
|
||||||
|
|
||||||
|
if css := shadowCSS(style, Shadow, session); css != "" {
|
||||||
|
builder.add("box-shadow", css)
|
||||||
|
}
|
||||||
|
|
||||||
|
if css := shadowCSS(style, TextShadow, session); css != "" {
|
||||||
|
builder.add("text-shadow", css)
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := style.properties[ColumnSeparator]; ok {
|
||||||
|
if separator, ok := value.(ColumnSeparatorProperty); ok {
|
||||||
|
if css := separator.cssValue(session); css != "" {
|
||||||
|
builder.add("column-rule", css)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if avoid, ok := boolProperty(style, AvoidBreak, session); ok {
|
||||||
|
if avoid {
|
||||||
|
builder.add("break-inside", "avoid")
|
||||||
|
} else {
|
||||||
|
builder.add("break-inside", "auto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap, _ := enumProperty(style, Wrap, session, 0)
|
||||||
|
orientation, ok := getOrientation(style, session)
|
||||||
|
if ok || wrap > 0 {
|
||||||
|
cssText := enumProperties[Orientation].cssValues[orientation]
|
||||||
|
switch wrap {
|
||||||
|
case WrapOn:
|
||||||
|
cssText += " wrap"
|
||||||
|
|
||||||
|
case WrapReverse:
|
||||||
|
cssText += " wrap-reverse"
|
||||||
|
}
|
||||||
|
builder.add(`flex-flow`, cssText)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := (orientation == StartToEndOrientation || orientation == EndToStartOrientation)
|
||||||
|
|
||||||
|
var hAlignTag, vAlignTag string
|
||||||
|
if rows {
|
||||||
|
hAlignTag = `justify-content`
|
||||||
|
vAlignTag = `align-items`
|
||||||
|
} else {
|
||||||
|
hAlignTag = `align-items`
|
||||||
|
vAlignTag = `justify-content`
|
||||||
|
}
|
||||||
|
|
||||||
|
if align, ok := enumProperty(style, HorizontalAlign, session, LeftAlign); ok {
|
||||||
|
switch align {
|
||||||
|
case LeftAlign:
|
||||||
|
if (!rows && wrap == WrapReverse) || orientation == EndToStartOrientation {
|
||||||
|
builder.add(hAlignTag, `flex-end`)
|
||||||
|
} else {
|
||||||
|
builder.add(hAlignTag, `flex-start`)
|
||||||
|
}
|
||||||
|
case RightAlign:
|
||||||
|
if (!rows && wrap == WrapReverse) || orientation == EndToStartOrientation {
|
||||||
|
builder.add(hAlignTag, `flex-start`)
|
||||||
|
} else {
|
||||||
|
builder.add(hAlignTag, `flex-end`)
|
||||||
|
}
|
||||||
|
case CenterAlign:
|
||||||
|
builder.add(hAlignTag, `center`)
|
||||||
|
|
||||||
|
case StretchAlign:
|
||||||
|
if rows {
|
||||||
|
builder.add(hAlignTag, `space-between`)
|
||||||
|
} else {
|
||||||
|
builder.add(hAlignTag, `stretch`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if align, ok := enumProperty(style, VerticalAlign, session, LeftAlign); ok {
|
||||||
|
switch align {
|
||||||
|
case TopAlign:
|
||||||
|
if (rows && wrap == WrapReverse) || orientation == BottomUpOrientation {
|
||||||
|
builder.add(vAlignTag, `flex-end`)
|
||||||
|
} else {
|
||||||
|
builder.add(vAlignTag, `flex-start`)
|
||||||
|
}
|
||||||
|
case BottomAlign:
|
||||||
|
if (rows && wrap == WrapReverse) || orientation == BottomUpOrientation {
|
||||||
|
builder.add(vAlignTag, `flex-start`)
|
||||||
|
} else {
|
||||||
|
builder.add(vAlignTag, `flex-end`)
|
||||||
|
}
|
||||||
|
case CenterAlign:
|
||||||
|
builder.add(vAlignTag, `center`)
|
||||||
|
|
||||||
|
case StretchAlign:
|
||||||
|
if rows {
|
||||||
|
builder.add(hAlignTag, `stretch`)
|
||||||
|
} else {
|
||||||
|
builder.add(hAlignTag, `space-between`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r, ok := rangeProperty(style, Row, session); ok {
|
||||||
|
builder.add("grid-row-start", strconv.Itoa(r.First+1))
|
||||||
|
builder.add("grid-row-end", strconv.Itoa(r.Last+2))
|
||||||
|
}
|
||||||
|
if r, ok := rangeProperty(style, Column, session); ok {
|
||||||
|
builder.add("grid-column-start", strconv.Itoa(r.First+1))
|
||||||
|
builder.add("grid-column-end", strconv.Itoa(r.Last+2))
|
||||||
|
}
|
||||||
|
if text := style.gridCellSizesCSS(CellWidth, session); text != "" {
|
||||||
|
builder.add(`grid-template-columns`, text)
|
||||||
|
}
|
||||||
|
if text := style.gridCellSizesCSS(CellHeight, session); text != "" {
|
||||||
|
builder.add(`grid-template-rows`, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
style.writeViewTransformCSS(builder, session)
|
||||||
|
|
||||||
|
if clip := getClipShape(style, Clip, session); clip != nil && clip.valid(session) {
|
||||||
|
builder.add(`clip-path`, clip.cssStyle(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
if clip := getClipShape(style, ShapeOutside, session); clip != nil && clip.valid(session) {
|
||||||
|
builder.add(`shape-outside`, clip.cssStyle(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
if value := style.getRaw(Filter); value != nil {
|
||||||
|
if filter, ok := value.(ViewFilter); ok {
|
||||||
|
if text := filter.cssStyle(session); text != "" {
|
||||||
|
builder.add(`filter`, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
if len(style.transitions) > 0 {
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
for property, transition := range style.transitions {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteString(`, `)
|
||||||
|
}
|
||||||
|
buffer.WriteString(property)
|
||||||
|
transition.cssWrite(buffer, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
builder.add(`transition`, buffer.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TODO text-shadow
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getOrientation(style Properties, session Session) (int, bool) {
|
||||||
|
if value := style.Get(Orientation); value != nil {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case int:
|
||||||
|
return value, true
|
||||||
|
|
||||||
|
case string:
|
||||||
|
text, ok := session.resolveConstants(value)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
text = strings.ToLower(strings.Trim(text, " \t\n\r"))
|
||||||
|
switch text {
|
||||||
|
case "vertical":
|
||||||
|
return TopDownOrientation, true
|
||||||
|
|
||||||
|
case "horizontal":
|
||||||
|
return StartToEndOrientation, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if result, ok := enumStringToInt(text, enumProperties[Orientation].values, true); ok {
|
||||||
|
return result, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) Get(tag string) interface{} {
|
||||||
|
return style.get(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) get(tag string) interface{} {
|
||||||
|
switch tag {
|
||||||
|
case Border, CellBorder:
|
||||||
|
return getBorder(&style.propertyList, tag)
|
||||||
|
|
||||||
|
case BorderLeft, BorderRight, BorderTop, BorderBottom,
|
||||||
|
BorderStyle, BorderLeftStyle, BorderRightStyle, BorderTopStyle, BorderBottomStyle,
|
||||||
|
BorderColor, BorderLeftColor, BorderRightColor, BorderTopColor, BorderBottomColor,
|
||||||
|
BorderWidth, BorderLeftWidth, BorderRightWidth, BorderTopWidth, BorderBottomWidth:
|
||||||
|
if border := getBorder(style, Border); border != nil {
|
||||||
|
return border.Get(tag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case CellBorderLeft, CellBorderRight, CellBorderTop, CellBorderBottom,
|
||||||
|
CellBorderStyle, CellBorderLeftStyle, CellBorderRightStyle, CellBorderTopStyle, CellBorderBottomStyle,
|
||||||
|
CellBorderColor, CellBorderLeftColor, CellBorderRightColor, CellBorderTopColor, CellBorderBottomColor,
|
||||||
|
CellBorderWidth, CellBorderLeftWidth, CellBorderRightWidth, CellBorderTopWidth, CellBorderBottomWidth:
|
||||||
|
if border := getBorder(style, CellBorder); border != nil {
|
||||||
|
return border.Get(tag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
|
||||||
|
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
|
||||||
|
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
|
||||||
|
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
|
||||||
|
return getRadiusElement(style, tag)
|
||||||
|
|
||||||
|
case ColumnSeparator:
|
||||||
|
if val, ok := style.properties[ColumnSeparator]; ok {
|
||||||
|
return val.(ColumnSeparatorProperty)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case ColumnSeparatorStyle, ColumnSeparatorWidth, ColumnSeparatorColor:
|
||||||
|
if val, ok := style.properties[ColumnSeparator]; ok {
|
||||||
|
separator := val.(ColumnSeparatorProperty)
|
||||||
|
return separator.Get(tag)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return style.propertyList.getRaw(tag)
|
||||||
|
}
|
|
@ -0,0 +1,273 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (style *viewStyle) setRange(tag string, value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case string:
|
||||||
|
if strings.Contains(value, "@") {
|
||||||
|
style.properties[tag] = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var r Range
|
||||||
|
if !r.setValue(value) {
|
||||||
|
invalidPropertyValue(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
style.properties[tag] = r
|
||||||
|
|
||||||
|
case int:
|
||||||
|
style.properties[tag] = Range{First: value, Last: value}
|
||||||
|
|
||||||
|
case Range:
|
||||||
|
style.properties[tag] = value
|
||||||
|
|
||||||
|
default:
|
||||||
|
notCompatibleType(tag, value)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) setBackground(value interface{}) bool {
|
||||||
|
switch value := value.(type) {
|
||||||
|
case BackgroundElement:
|
||||||
|
style.properties[Background] = []BackgroundElement{value}
|
||||||
|
return true
|
||||||
|
|
||||||
|
case []BackgroundElement:
|
||||||
|
style.properties[Background] = value
|
||||||
|
return true
|
||||||
|
|
||||||
|
case DataObject:
|
||||||
|
if element := createBackground(value); element != nil {
|
||||||
|
style.properties[Background] = []BackgroundElement{element}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case []DataObject:
|
||||||
|
for _, obj := range value {
|
||||||
|
background := []BackgroundElement{}
|
||||||
|
if element := createBackground(obj); element != nil {
|
||||||
|
background = append(background, element)
|
||||||
|
}
|
||||||
|
if len(background) > 0 {
|
||||||
|
style.properties[Background] = background
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case string:
|
||||||
|
if obj := ParseDataText(value); obj != nil {
|
||||||
|
if element := createBackground(obj); element != nil {
|
||||||
|
style.properties[Background] = []BackgroundElement{element}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) Remove(tag string) {
|
||||||
|
style.remove(strings.ToLower(tag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) remove(tag string) {
|
||||||
|
switch tag {
|
||||||
|
case BorderStyle, BorderColor, BorderWidth,
|
||||||
|
BorderLeft, BorderLeftStyle, BorderLeftColor, BorderLeftWidth,
|
||||||
|
BorderRight, BorderRightStyle, BorderRightColor, BorderRightWidth,
|
||||||
|
BorderTop, BorderTopStyle, BorderTopColor, BorderTopWidth,
|
||||||
|
BorderBottom, BorderBottomStyle, BorderBottomColor, BorderBottomWidth:
|
||||||
|
if border := getBorder(style, Border); border != nil {
|
||||||
|
border.delete(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
case CellBorderStyle, CellBorderColor, CellBorderWidth,
|
||||||
|
CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth,
|
||||||
|
CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth,
|
||||||
|
CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth,
|
||||||
|
CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth:
|
||||||
|
if border := getBorder(style, CellBorder); border != nil {
|
||||||
|
border.delete(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
case MarginTop, MarginRight, MarginBottom, MarginLeft,
|
||||||
|
"top-margin", "right-margin", "bottom-margin", "left-margin":
|
||||||
|
style.removeBoundsSide(Margin, tag)
|
||||||
|
|
||||||
|
case PaddingTop, PaddingRight, PaddingBottom, PaddingLeft,
|
||||||
|
"top-padding", "right-padding", "bottom-padding", "left-padding":
|
||||||
|
style.removeBoundsSide(Padding, tag)
|
||||||
|
|
||||||
|
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft:
|
||||||
|
style.removeBoundsSide(CellPadding, tag)
|
||||||
|
|
||||||
|
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
|
||||||
|
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
|
||||||
|
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
|
||||||
|
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
|
||||||
|
style.removeRadiusElement(tag)
|
||||||
|
|
||||||
|
case OutlineStyle, OutlineWidth, OutlineColor:
|
||||||
|
if outline := getOutline(style); outline != nil {
|
||||||
|
outline.Remove(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
style.propertyList.remove(tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) Set(tag string, value interface{}) bool {
|
||||||
|
return style.set(strings.ToLower(tag), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) set(tag string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
style.remove(tag)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Shadow, TextShadow:
|
||||||
|
return style.setShadow(tag, value)
|
||||||
|
|
||||||
|
case Background:
|
||||||
|
return style.setBackground(value)
|
||||||
|
|
||||||
|
case Border, CellBorder:
|
||||||
|
if border := newBorderProperty(value); border != nil {
|
||||||
|
style.properties[tag] = border
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
case BorderStyle, BorderColor, BorderWidth,
|
||||||
|
BorderLeft, BorderLeftStyle, BorderLeftColor, BorderLeftWidth,
|
||||||
|
BorderRight, BorderRightStyle, BorderRightColor, BorderRightWidth,
|
||||||
|
BorderTop, BorderTopStyle, BorderTopColor, BorderTopWidth,
|
||||||
|
BorderBottom, BorderBottomStyle, BorderBottomColor, BorderBottomWidth:
|
||||||
|
|
||||||
|
border := getBorder(style, Border)
|
||||||
|
if border == nil {
|
||||||
|
border = NewBorder(nil)
|
||||||
|
if border.Set(tag, value) {
|
||||||
|
style.properties[Border] = border
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return border.Set(tag, value)
|
||||||
|
|
||||||
|
case CellBorderStyle, CellBorderColor, CellBorderWidth,
|
||||||
|
CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth,
|
||||||
|
CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth,
|
||||||
|
CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth,
|
||||||
|
CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth:
|
||||||
|
|
||||||
|
border := getBorder(style, CellBorder)
|
||||||
|
if border == nil {
|
||||||
|
border = NewBorder(nil)
|
||||||
|
if border.Set(tag, value) {
|
||||||
|
style.properties[CellBorder] = border
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return border.Set(tag, value)
|
||||||
|
|
||||||
|
case Radius:
|
||||||
|
return style.setRadius(value)
|
||||||
|
|
||||||
|
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
|
||||||
|
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
|
||||||
|
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
|
||||||
|
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
|
||||||
|
return style.setRadiusElement(tag, value)
|
||||||
|
|
||||||
|
case Margin, Padding, CellPadding:
|
||||||
|
return style.setBounds(tag, value)
|
||||||
|
|
||||||
|
case MarginTop, MarginRight, MarginBottom, MarginLeft,
|
||||||
|
"top-margin", "right-margin", "bottom-margin", "left-margin":
|
||||||
|
return style.setBoundsSide(Margin, tag, value)
|
||||||
|
|
||||||
|
case PaddingTop, PaddingRight, PaddingBottom, PaddingLeft,
|
||||||
|
"top-padding", "right-padding", "bottom-padding", "left-padding":
|
||||||
|
return style.setBoundsSide(Padding, tag, value)
|
||||||
|
|
||||||
|
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft:
|
||||||
|
return style.setBoundsSide(CellPadding, tag, value)
|
||||||
|
|
||||||
|
case Outline:
|
||||||
|
return style.setOutline(value)
|
||||||
|
|
||||||
|
case OutlineStyle, OutlineWidth, OutlineColor:
|
||||||
|
if outline := getOutline(style); outline != nil {
|
||||||
|
return outline.Set(tag, value)
|
||||||
|
}
|
||||||
|
style.properties[Outline] = NewOutlineProperty(Params{tag: value})
|
||||||
|
return true
|
||||||
|
|
||||||
|
case Orientation:
|
||||||
|
if text, ok := value.(string); ok {
|
||||||
|
switch strings.ToLower(text) {
|
||||||
|
case "vertical":
|
||||||
|
style.properties[Orientation] = TopDownOrientation
|
||||||
|
return true
|
||||||
|
|
||||||
|
case "horizontal":
|
||||||
|
style.properties[Orientation] = StartToEndOrientation
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case TextWeight:
|
||||||
|
if n, ok := value.(int); ok && n >= 100 && n%100 == 0 {
|
||||||
|
n /= 100
|
||||||
|
if n > 0 && n <= 9 {
|
||||||
|
style.properties[TextWeight] = StartToEndOrientation
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case Row, Column:
|
||||||
|
return style.setRange(tag, value)
|
||||||
|
|
||||||
|
case CellWidth, CellHeight:
|
||||||
|
return style.setGridCellSize(tag, value)
|
||||||
|
|
||||||
|
case ColumnSeparator:
|
||||||
|
if separator := newColumnSeparatorProperty(value); separator != nil {
|
||||||
|
style.properties[ColumnSeparator] = separator
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case ColumnSeparatorStyle, ColumnSeparatorWidth, ColumnSeparatorColor:
|
||||||
|
var separator ColumnSeparatorProperty = nil
|
||||||
|
if val, ok := style.properties[ColumnSeparator]; ok {
|
||||||
|
separator = val.(ColumnSeparatorProperty)
|
||||||
|
}
|
||||||
|
if separator == nil {
|
||||||
|
separator = newColumnSeparatorProperty(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if separator.Set(tag, value) {
|
||||||
|
style.properties[ColumnSeparator] = separator
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
|
||||||
|
case Clip, ShapeOutside:
|
||||||
|
return style.setClipShape(tag, value)
|
||||||
|
|
||||||
|
case Filter:
|
||||||
|
return style.setFilter(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return style.propertyList.set(tag, value)
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
/*
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestViewStyleCreate(t *testing.T) {
|
||||||
|
|
||||||
|
app := new(application)
|
||||||
|
app.init("")
|
||||||
|
session := newSession(app, 1, "", false, false)
|
||||||
|
|
||||||
|
var style viewStyle
|
||||||
|
style.init()
|
||||||
|
|
||||||
|
data := []struct{ property, value string }{
|
||||||
|
{Width, "100%"},
|
||||||
|
{Height, "400px"},
|
||||||
|
{Margin, "4px"},
|
||||||
|
{Margin + "-bottom", "auto"},
|
||||||
|
{Padding, "1em"},
|
||||||
|
{Font, "Arial"},
|
||||||
|
{BackgroundColor, "#FF008000"},
|
||||||
|
{TextColor, "#FF000000"},
|
||||||
|
{TextSize, "1.25em"},
|
||||||
|
{TextWeight, "bold"},
|
||||||
|
{TextAlign, "center"},
|
||||||
|
{TextTransform, "uppercase"},
|
||||||
|
{TextIndent, "0.25em"},
|
||||||
|
{LetterSpacing, "1.5em"},
|
||||||
|
{WordSpacing, "8px"},
|
||||||
|
{LineHeight, "2em"},
|
||||||
|
{Italic, "on"},
|
||||||
|
{TextDecoration, "strikethrough | overline | underline"},
|
||||||
|
{SmallCaps, "on"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prop := range data {
|
||||||
|
style.Set(prop.property, prop.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
style.AddShadow(NewViewShadow(SizeUnit{Auto, 0}, SizeUnit{Auto, 0}, Px(4), Px(6), 0xFF808080))
|
||||||
|
|
||||||
|
expected := `width: 100%; height: 400px; font-size: 1.25rem; text-indent: 0.25rem; letter-spacing: 1.5rem; word-spacing: 8px; ` +
|
||||||
|
`line-height: 2rem; padding: 1rem; margin-left: 4px; margin-top: 4px; margin-right: 4px; box-shadow: 0 0 4px 6px rgb(128,128,128); ` +
|
||||||
|
`background-color: rgb(0,128,0); color: rgb(0,0,0); font-family: Arial; font-weight: bold; font-style: italic; font-variant: small-caps; ` +
|
||||||
|
`text-align: center; text-decoration: line-through overline underline; text-transform: uppercase;`
|
||||||
|
|
||||||
|
buffer := strings.Builder{}
|
||||||
|
style.cssViewStyle(&buffer, session)
|
||||||
|
if text := strings.Trim(buffer.String(), " "); text != expected {
|
||||||
|
t.Error("\nresult : " + text + "\nexpected: " + expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := newCompactDataWriter()
|
||||||
|
w.StartObject("_")
|
||||||
|
style.writeStyle(w)
|
||||||
|
w.FinishObject()
|
||||||
|
expected2 := `_{width=100%,height=400px,margin="4px,4px,auto,4px",padding=1em,background-color=#FF008000,shadow=_{color=#FF808080,blur=4px,spread-radius=6px},font=Arial,text-color=#FF000000,text-size=1.25em,text-weight=bold,italic=on,small-caps=on,text-decoration=strikethrough|overline|underline,text-align=center,text-indent=0.25em,letter-spacing=1.5em,word-spacing=8px,line-height=2em,text-transform=uppercase}`
|
||||||
|
|
||||||
|
if text := w.String(); text != expected2 {
|
||||||
|
t.Error("\n result: " + text + "\nexpected: " + expected2)
|
||||||
|
}
|
||||||
|
|
||||||
|
var style1 viewStyle
|
||||||
|
style1.init()
|
||||||
|
if obj, err := ParseDataText(expected2); err == nil {
|
||||||
|
style1.parseStyle(obj, new(sessionData))
|
||||||
|
buffer.Reset()
|
||||||
|
style.cssStyle(&buffer)
|
||||||
|
if text := buffer.String(); text != expected {
|
||||||
|
t.Error("\n result: " + text + "\nexpected: " + expected)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var style2 viewStyle
|
||||||
|
style2.init()
|
||||||
|
|
||||||
|
style2.textWeight = 4
|
||||||
|
style2.textAlign = RightAlign
|
||||||
|
style2.textTransform = LowerCaseTextTransform
|
||||||
|
style2.textDecoration = NoneDecoration
|
||||||
|
style2.italic = Off
|
||||||
|
style2.smallCaps = Off
|
||||||
|
|
||||||
|
expected = `font-weight: normal; font-style: normal; font-variant: normal; text-align: right; text-decoration: none; text-transform: lowercase; `
|
||||||
|
buffer.Reset()
|
||||||
|
style2.cssStyle(&buffer)
|
||||||
|
if text := buffer.String(); text != expected {
|
||||||
|
t.Error("\n result: " + text + "\nexpected: " + expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Reset()
|
||||||
|
w.StartObject("_")
|
||||||
|
style2.writeStyle(w)
|
||||||
|
w.FinishObject()
|
||||||
|
expected = `_{text-weight=normal,italic=off,small-caps=off,text-decoration=none,text-align=right,text-transform=lowercase}`
|
||||||
|
|
||||||
|
if text := w.String(); text != expected {
|
||||||
|
t.Error("\n result: " + text + "\nexpected: " + expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
style2.textWeight = 5
|
||||||
|
style2.textAlign = JustifyTextAlign
|
||||||
|
style2.textTransform = CapitalizeTextTransform
|
||||||
|
style2.textDecoration = Inherit
|
||||||
|
style2.italic = Inherit
|
||||||
|
style2.smallCaps = Inherit
|
||||||
|
|
||||||
|
expected = `font-weight: 500; text-align: justify; text-transform: capitalize; `
|
||||||
|
buffer.Reset()
|
||||||
|
style2.cssStyle(&buffer)
|
||||||
|
if text := buffer.String(); text != expected {
|
||||||
|
t.Error("\n result: " + text + "\nexpected: " + expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Reset()
|
||||||
|
w.StartObject("_")
|
||||||
|
style2.writeStyle(w)
|
||||||
|
w.FinishObject()
|
||||||
|
expected = `_{text-weight=5,text-align=justify,text-transform=capitalize}`
|
||||||
|
|
||||||
|
if text := w.String(); text != expected {
|
||||||
|
t.Error("\n result: " + text + "\nexpected: " + expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
|
@ -0,0 +1,299 @@
|
||||||
|
package rui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Perspective is the name of the SizeUnit property that determines the distance between the z = 0 plane
|
||||||
|
// and the user in order to give a 3D-positioned element some perspective. Each 3D element
|
||||||
|
// with z > 0 becomes larger; each 3D-element with z < 0 becomes smaller.
|
||||||
|
// The default value is 0 (no 3D effects).
|
||||||
|
Perspective = "perspective"
|
||||||
|
// PerspectiveOriginX is the name of the SizeUnit property that determines the x-coordinate of the position
|
||||||
|
// at which the viewer is looking. It is used as the vanishing point by the Perspective property.
|
||||||
|
// The default value is 50%.
|
||||||
|
PerspectiveOriginX = "perspective-origin-x"
|
||||||
|
// PerspectiveOriginY is the name of the SizeUnit property that determines the y-coordinate of the position
|
||||||
|
// at which the viewer is looking. It is used as the vanishing point by the Perspective property.
|
||||||
|
// The default value is 50%.
|
||||||
|
PerspectiveOriginY = "perspective-origin-y"
|
||||||
|
// BackfaceVisible is the name of the bool property that sets whether the back face of an element is
|
||||||
|
// visible when turned towards the user. Values:
|
||||||
|
// true - the back face is visible when turned towards the user (default value).
|
||||||
|
// false - the back face is hidden, effectively making the element invisible when turned away from the user.
|
||||||
|
BackfaceVisible = "backface-visibility"
|
||||||
|
// OriginX is the name of the SizeUnit property that determines the x-coordinate of the point around which
|
||||||
|
// a view transformation is applied.
|
||||||
|
// The default value is 50%.
|
||||||
|
OriginX = "origin-x"
|
||||||
|
// OriginY is the name of the SizeUnit property that determines the y-coordinate of the point around which
|
||||||
|
// a view transformation is applied.
|
||||||
|
// The default value is 50%.
|
||||||
|
OriginY = "origin-y"
|
||||||
|
// OriginZ is the name of the SizeUnit property that determines the z-coordinate of the point around which
|
||||||
|
// a view transformation is applied.
|
||||||
|
// The default value is 50%.
|
||||||
|
OriginZ = "origin-z"
|
||||||
|
// TranslateX is the name of the SizeUnit property that specify the x-axis translation value
|
||||||
|
// of a 2D/3D translation
|
||||||
|
TranslateX = "translate-x"
|
||||||
|
// TranslateY is the name of the SizeUnit property that specify the y-axis translation value
|
||||||
|
// of a 2D/3D translation
|
||||||
|
TranslateY = "translate-y"
|
||||||
|
// TranslateZ is the name of the SizeUnit property that specify the z-axis translation value
|
||||||
|
// of a 3D translation
|
||||||
|
TranslateZ = "translate-z"
|
||||||
|
// ScaleX is the name of the float property that specify the x-axis scaling value of a 2D/3D scale
|
||||||
|
// The default value is 1.
|
||||||
|
ScaleX = "scale-x"
|
||||||
|
// ScaleY is the name of the float property that specify the y-axis scaling value of a 2D/3D scale
|
||||||
|
// The default value is 1.
|
||||||
|
ScaleY = "scale-y"
|
||||||
|
// ScaleZ is the name of the float property that specify the z-axis scaling value of a 3D scale
|
||||||
|
// The default value is 1.
|
||||||
|
ScaleZ = "scale-z"
|
||||||
|
// Rotate is the name of the AngleUnit property that determines the angle of the view rotation.
|
||||||
|
// A positive angle denotes a clockwise rotation, a negative angle a counter-clockwise one.
|
||||||
|
Rotate = "rotate"
|
||||||
|
// RotateX is the name of the float property that determines the x-coordinate of the vector denoting
|
||||||
|
// the axis of rotation which could between 0 and 1.
|
||||||
|
RotateX = "rotate-x"
|
||||||
|
// RotateY is the name of the float property that determines the y-coordinate of the vector denoting
|
||||||
|
// the axis of rotation which could between 0 and 1.
|
||||||
|
RotateY = "rotate-y"
|
||||||
|
// RotateZ is the name of the float property that determines the z-coordinate of the vector denoting
|
||||||
|
// the axis of rotation which could between 0 and 1.
|
||||||
|
RotateZ = "rotate-z"
|
||||||
|
// SkewX is the name of the AngleUnit property that representing the angle to use to distort
|
||||||
|
// the element along the abscissa. The default value is 0.
|
||||||
|
SkewX = "skew-x"
|
||||||
|
// SkewY is the name of the AngleUnit property that representing the angle to use to distort
|
||||||
|
// the element along the ordinate. The default value is 0.
|
||||||
|
SkewY = "skew-y"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTransform3D(style Properties, session Session) bool {
|
||||||
|
perspective, ok := sizeProperty(style, Perspective, session)
|
||||||
|
return ok && perspective.Type != Auto && perspective.Value != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPerspectiveOrigin(style Properties, session Session) (SizeUnit, SizeUnit) {
|
||||||
|
x, _ := sizeProperty(style, PerspectiveOriginX, session)
|
||||||
|
y, _ := sizeProperty(style, PerspectiveOriginY, session)
|
||||||
|
return x, y
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrigin(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) {
|
||||||
|
x, _ := sizeProperty(style, OriginX, session)
|
||||||
|
y, _ := sizeProperty(style, OriginY, session)
|
||||||
|
z, _ := sizeProperty(style, OriginZ, session)
|
||||||
|
return x, y, z
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSkew(style Properties, session Session) (AngleUnit, AngleUnit) {
|
||||||
|
skewX, _ := angleProperty(style, SkewX, session)
|
||||||
|
skewY, _ := angleProperty(style, SkewY, session)
|
||||||
|
return skewX, skewY
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTranslate(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) {
|
||||||
|
x, _ := sizeProperty(style, TranslateX, session)
|
||||||
|
y, _ := sizeProperty(style, TranslateY, session)
|
||||||
|
z, _ := sizeProperty(style, TranslateZ, session)
|
||||||
|
return x, y, z
|
||||||
|
}
|
||||||
|
|
||||||
|
func getScale(style Properties, session Session) (float64, float64, float64) {
|
||||||
|
scaleX, _ := floatProperty(style, ScaleX, session, 1)
|
||||||
|
scaleY, _ := floatProperty(style, ScaleY, session, 1)
|
||||||
|
scaleZ, _ := floatProperty(style, ScaleZ, session, 1)
|
||||||
|
return scaleX, scaleY, scaleZ
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRotate(style Properties, session Session) (float64, float64, float64, AngleUnit) {
|
||||||
|
rotateX, _ := floatProperty(style, RotateX, session, 1)
|
||||||
|
rotateY, _ := floatProperty(style, RotateY, session, 1)
|
||||||
|
rotateZ, _ := floatProperty(style, RotateZ, session, 1)
|
||||||
|
angle, _ := angleProperty(style, Rotate, session)
|
||||||
|
return rotateX, rotateY, rotateZ, angle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) transform(session Session) string {
|
||||||
|
|
||||||
|
buffer := allocStringBuilder()
|
||||||
|
defer freeStringBuilder(buffer)
|
||||||
|
|
||||||
|
skewX, skewY := getSkew(style, session)
|
||||||
|
if skewX.Value != 0 || skewY.Value != 0 {
|
||||||
|
buffer.WriteString(`skew(`)
|
||||||
|
buffer.WriteString(skewX.cssString())
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(skewY.cssString())
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y, z := getTranslate(style, session)
|
||||||
|
scaleX, scaleY, scaleZ := getScale(style, session)
|
||||||
|
if getTransform3D(style, session) {
|
||||||
|
if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) || (z.Type != Auto && z.Value != 0) {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(`translate3d(`)
|
||||||
|
buffer.WriteString(x.cssString("0"))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(y.cssString("0"))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(z.cssString("0"))
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
if scaleX != 1 || scaleY != 1 || scaleZ != 1 {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(`scale3d(`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(scaleX, 'g', -1, 64))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(strconv.FormatFloat(scaleY, 'g', -1, 64))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(strconv.FormatFloat(scaleZ, 'g', -1, 64))
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
rotateX, rotateY, rotateZ, angle := getRotate(style, session)
|
||||||
|
if angle.Value != 0 && (rotateX != 0 || rotateY != 0 || rotateZ != 0) {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(`rotate3d(`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(rotateX, 'g', -1, 64))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(strconv.FormatFloat(rotateY, 'g', -1, 64))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(strconv.FormatFloat(rotateZ, 'g', -1, 64))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(angle.cssString())
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(`translate(`)
|
||||||
|
buffer.WriteString(x.cssString("0"))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(y.cssString("0"))
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
if scaleX != 1 || scaleY != 1 {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(`scale(`)
|
||||||
|
buffer.WriteString(strconv.FormatFloat(scaleX, 'g', -1, 64))
|
||||||
|
buffer.WriteRune(',')
|
||||||
|
buffer.WriteString(strconv.FormatFloat(scaleY, 'g', -1, 64))
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
angle, _ := angleProperty(style, Rotate, session)
|
||||||
|
if angle.Value != 0 {
|
||||||
|
if buffer.Len() > 0 {
|
||||||
|
buffer.WriteRune(' ')
|
||||||
|
}
|
||||||
|
buffer.WriteString(`rotate(`)
|
||||||
|
buffer.WriteString(angle.cssString())
|
||||||
|
buffer.WriteRune(')')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (style *viewStyle) writeViewTransformCSS(builder cssBuilder, session Session) {
|
||||||
|
if getTransform3D(style, session) {
|
||||||
|
if perspective, ok := sizeProperty(style, Perspective, session); ok && perspective.Type != Auto && perspective.Value != 0 {
|
||||||
|
builder.add(`perspective`, perspective.cssString("0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y := getPerspectiveOrigin(style, session)
|
||||||
|
if x.Type != Auto || y.Type != Auto {
|
||||||
|
builder.addValues(`perspective-origin`, ` `, x.cssString("50%"), y.cssString("50%"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if backfaceVisible, ok := boolProperty(style, BackfaceVisible, session); ok {
|
||||||
|
if backfaceVisible {
|
||||||
|
builder.add(`backface-visibility`, `visible`)
|
||||||
|
} else {
|
||||||
|
builder.add(`backface-visibility`, `hidden`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
x, y, z := getOrigin(style, session)
|
||||||
|
if x.Type != Auto || y.Type != Auto || z.Type != Auto {
|
||||||
|
builder.addValues(`transform-origin`, ` `, x.cssString("50%"), y.cssString("50%"), z.cssString("0"))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
x, y, _ := getOrigin(style, session)
|
||||||
|
if x.Type != Auto || y.Type != Auto {
|
||||||
|
builder.addValues(`transform-origin`, ` `, x.cssString("50%"), y.cssString("50%"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add(`transform`, style.transform(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (view *viewData) updateTransformProperty(tag string) bool {
|
||||||
|
htmlID := view.htmlID()
|
||||||
|
session := view.session
|
||||||
|
|
||||||
|
switch tag {
|
||||||
|
case Perspective:
|
||||||
|
updateCSSStyle(htmlID, session)
|
||||||
|
|
||||||
|
case PerspectiveOriginX, PerspectiveOriginY:
|
||||||
|
if getTransform3D(view, session) {
|
||||||
|
x, y := GetPerspectiveOrigin(view, "")
|
||||||
|
value := ""
|
||||||
|
if x.Type != Auto || y.Type != Auto {
|
||||||
|
value = x.cssString("50%") + " " + y.cssString("50%")
|
||||||
|
}
|
||||||
|
updateCSSProperty(htmlID, "perspective-origin", value, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
case BackfaceVisible:
|
||||||
|
if getTransform3D(view, session) {
|
||||||
|
if GetBackfaceVisible(view, "") {
|
||||||
|
updateCSSProperty(htmlID, BackfaceVisible, "visible", session)
|
||||||
|
} else {
|
||||||
|
updateCSSProperty(htmlID, BackfaceVisible, "hidden", session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case OriginX, OriginY, OriginZ:
|
||||||
|
x, y, z := getOrigin(view, session)
|
||||||
|
value := ""
|
||||||
|
if getTransform3D(view, session) {
|
||||||
|
if x.Type != Auto || y.Type != Auto || z.Type != Auto {
|
||||||
|
value = x.cssString("50%") + " " + y.cssString("50%") + " " + z.cssString("50%")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if x.Type != Auto || y.Type != Auto {
|
||||||
|
value = x.cssString("50%") + " " + y.cssString("50%")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateCSSProperty(htmlID, "transform-origin", value, session)
|
||||||
|
|
||||||
|
case SkewX, SkewY, TranslateX, TranslateY, TranslateZ, ScaleX, ScaleY, ScaleZ, Rotate, RotateX, RotateY, RotateZ:
|
||||||
|
updateCSSProperty(htmlID, "transform", view.transform(session), session)
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue