Initialization

This commit is contained in:
anoshenko 2021-09-07 17:36:50 +03:00
parent bdb490c953
commit 73e7184395
104 changed files with 39103 additions and 2 deletions

14
.gitignore vendored Normal file
View File

@ -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

18
.vscode/launch.json vendored Normal file
View File

@ -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": []
}
]
}

13
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"cSpell.words": [
"anoshenko",
"helvetica",
"htmlid",
"nesw",
"nwse",
"onclick",
"onkeydown",
"onmousedown",
"upgrader"
]
}

4350
README-ru.md Normal file

File diff suppressed because it is too large Load Diff

4313
README.md

File diff suppressed because it is too large Load Diff

40
absoluteLayout.go Normal file
View File

@ -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)
}
}
}

212
angleUnit.go Normal file
View File

@ -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 (1400 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 (1400 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
}

229
animation.go Normal file
View File

@ -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
}
*/

74
appLog.go Normal file
View File

@ -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)
}
}
}

1231
app_scripts.js Normal file

File diff suppressed because it is too large Load Diff

124
app_styles.css Normal file
View File

@ -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;
}
}
*/

297
application.go Normal file
View File

@ -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
}

31
audioPlayer.go Normal file
View File

@ -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"
}

716
background.go Normal file
View File

@ -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()
}

710
border.go Normal file
View File

@ -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
}

405
bounds.go Normal file
View File

@ -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
}

99
bounds_test.go Normal file
View File

@ -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)
}
}
*/

36
button.go Normal file
View File

@ -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
}

1011
canvas.go Normal file

File diff suppressed because it is too large Load Diff

118
canvasView.go Normal file
View File

@ -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()
}
}

374
checkbox.go Normal file
View File

@ -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
}

177
color.go Normal file
View File

@ -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
}

448
colorConstants.go Normal file
View File

@ -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,
}

253
colorPicker.go Normal file
View File

@ -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){}
}

120
color_test.go Normal file
View File

@ -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)
}
}
}

222
columnLayout.go Normal file
View File

@ -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
}

184
columnSeparator.go Normal file
View File

@ -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()
}

258
cssBuilder.go Normal file
View File

@ -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`)
}

261
customView.go Normal file
View File

@ -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)
}
}

631
data.go Normal file
View File

@ -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
}

66
dataWriter_test.go Normal file
View File

@ -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 + "`")
}
}
*/

211
data_test.go Normal file
View File

@ -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)
}
}
}

404
datePicker.go Normal file
View File

@ -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){}
}

163
defaultTheme.rui Normal file
View File

@ -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,
}
],
}

177
detailsView.go Normal file
View File

@ -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
}

346
dropDownList.go Normal file
View File

@ -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
}

632
editView.go Normal file
View File

@ -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