mirror of https://github.com/anoshenko/rui.git
				
				
				
			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