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