forked from mbk-lab/rui_orig
2
0
Fork 0

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 != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if text, ok := stringProperty(view, Hint, view.Session()); ok {
return text
}
if text, ok := valueFromStyle(view, Hint); ok {
if text, ok = view.Session().resolveConstants(text); ok {
return text
}
}
}
return ""
}
// GetMaxLength returns a maximal lenght of EditView. If a maximal lenght is not limited then 0 is returned
// If the second argument (subviewID) is "" then a value of the first argument (view) is returned.
func GetMaxLength(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := intStyledProperty(view, MaxLength, 0); ok {
return result
}
}
return 0
}
// IsReadOnly returns the true if a EditView works in read only mode.
// If the second argument (subviewID) is "" then a value of the first argument (view) is returned.
func IsReadOnly(view View, subviewID string) bool {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := boolStyledProperty(view, ReadOnly); ok {
return result
}
}
return false
}
// IsSpellcheck returns a value of the Spellcheck property of EditView.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func IsSpellcheck(view View, subviewID string) bool {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if spellcheck, ok := boolStyledProperty(view, Spellcheck); ok {
return spellcheck
}
}
return false
}
// GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview.
// If there are no listeners then the empty list is returned
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTextChangedListeners(view View, subviewID string) []func(EditView, string) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(EditTextChangedEvent); value != nil {
if result, ok := value.([]func(EditView, string)); ok {
return result
}
}
}
return []func(EditView, string){}
}
// GetEditViewType returns a value of the Type property of EditView.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetEditViewType(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return SingleLineText
}
t, _ := enumStyledProperty(view, EditViewType, SingleLineText)
return t
}
// GetEditViewPattern returns a value of the Pattern property of EditView.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetEditViewPattern(view View, subviewID string) string {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok {
return pattern
}
if pattern, ok := valueFromStyle(view, EditViewPattern); ok {
if pattern, ok = view.Session().resolveConstants(pattern); ok {
return pattern
}
}
}
return ""
}
// IsEditViewWrap returns a value of the Wrap property of MultiLineEditView.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func IsEditViewWrap(view View, subviewID string) bool {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if wrap, ok := boolStyledProperty(view, Wrap); ok {
return wrap
}
}
return false
}
// AppendEditText appends the text to the EditView content.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func AppendEditText(view View, subviewID string, text string) {
if subviewID != "" {
if edit := EditViewByID(view, subviewID); edit != nil {
edit.AppendText(text)
return
}
}
if edit, ok := view.(EditView); ok {
edit.AppendText(text)
}
}

158
focusEvents.go Normal file
View File

@ -0,0 +1,158 @@
package rui
import "strings"
const (
// FocusEvent is the constant for "focus-event" property tag
// The "focus-event" event occurs when the View takes input focus.
// The main listener format: func(View).
// The additional listener format: func().
FocusEvent = "focus-event"
// LostFocusEvent is the constant for "lost-focus-event" property tag
// The "lost-focus-event" event occurs when the View lost input focus.
// The main listener format: func(View).
// The additional listener format: func().
LostFocusEvent = "lost-focus-event"
)
func valueToFocusListeners(value interface{}) ([]func(View), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(View):
return []func(View){value}, true
case func():
fn := func(View) {
value()
}
return []func(View){fn}, true
case []func(View):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(View) {
v()
}
}
return listeners, true
case []interface{}:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(View):
listeners[i] = v
case func():
listeners[i] = func(View) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var focusEvents = map[string]struct{ jsEvent, jsFunc string }{
FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"},
LostFocusEvent: {jsEvent: "onblur", jsFunc: "blurEvent"},
}
func (view *viewData) setFocusListener(tag string, value interface{}) bool {
listeners, ok := valueToFocusListeners(value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeFocusListener(tag)
} else if js, ok := focusEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeFocusListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := focusEvents[tag]; ok {
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
}
}
}
func getFocusListeners(view View, subviewID string, tag string) []func(View) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(View)); ok {
return result
}
}
}
return []func(View){}
}
func focusEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range focusEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
// GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetFocusListeners(view View, subviewID string) []func(View) {
return getFocusListeners(view, subviewID, FocusEvent)
}
// GetLostFocusListeners returns a LostFocusListener list. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetLostFocusListeners(view View, subviewID string) []func(View) {
return getFocusListeners(view, subviewID, LostFocusEvent)
}

5
go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/anoshenko/rui
go 1.17
require github.com/gorilla/websocket v1.4.2

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=

391
gridLayout.go Normal file
View File

@ -0,0 +1,391 @@
package rui
import (
"fmt"
"strings"
)
// GridLayout - grid-container of View
type GridLayout interface {
ViewsContainer
}
type gridLayoutData struct {
viewsContainerData
}
// NewGridLayout create new GridLayout object and return it
func NewGridLayout(session Session, params Params) GridLayout {
view := new(gridLayoutData)
view.Init(session)
setInitParams(view, params)
return view
}
func newGridLayout(session Session) View {
return NewGridLayout(session, nil)
}
// Init initialize fields of GridLayout by default values
func (gridLayout *gridLayoutData) Init(session Session) {
gridLayout.viewsContainerData.Init(session)
gridLayout.tag = "GridLayout"
gridLayout.systemClass = "ruiGridLayout"
}
func (style *viewStyle) setGridCellSize(tag string, value interface{}) bool {
setValues := func(values []string) bool {
count := len(values)
if count > 1 {
sizes := make([]interface{}, count)
for i, val := range values {
val = strings.Trim(val, " \t\n\r")
if isConstantName(val) {
sizes[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
sizes[i] = size
} else {
invalidPropertyValue(tag, value)
return false
}
}
style.properties[tag] = sizes
} else if isConstantName(values[0]) {
style.properties[tag] = values[0]
} else if size, ok := StringToSizeUnit(values[0]); ok {
style.properties[tag] = size
} else {
invalidPropertyValue(tag, value)
return false
}
return true
}
switch tag {
case CellWidth, CellHeight:
switch value := value.(type) {
case SizeUnit, []SizeUnit:
style.properties[tag] = value
case string:
if !setValues(strings.Split(value, ",")) {
return false
}
case []string:
if !setValues(value) {
return false
}
case []DataValue:
count := len(value)
if count == 0 {
invalidPropertyValue(tag, value)
return false
}
values := make([]string, count)
for i, val := range value {
if val.IsObject() {
invalidPropertyValue(tag, value)
return false
}
values[i] = val.Value()
}
if !setValues(values) {
return false
}
case []interface{}:
count := len(value)
if count == 0 {
invalidPropertyValue(tag, value)
return false
}
sizes := make([]interface{}, count)
for i, val := range value {
switch val := val.(type) {
case SizeUnit:
sizes[i] = val
case string:
if isConstantName(val) {
sizes[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
sizes[i] = size
} else {
invalidPropertyValue(tag, value)
return false
}
default:
invalidPropertyValue(tag, value)
return false
}
}
style.properties[tag] = sizes
default:
notCompatibleType(tag, value)
return false
}
return true
}
return false
}
func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string {
switch cellSize := gridCellSizes(style, tag, session); len(cellSize) {
case 0:
case 1:
if cellSize[0].Type != Auto {
return `repeat(auto-fill, ` + cellSize[0].cssString(`auto`) + `)`
}
default:
allAuto := true
allEqual := true
for i, size := range cellSize {
if size.Type != Auto {
allAuto = false
}
if i > 0 && !size.Equal(cellSize[0]) {
allEqual = false
}
}
if !allAuto {
if allEqual {
return fmt.Sprintf(`repeat(%d, %s)`, len(cellSize), cellSize[0].cssString(`auto`))
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, size := range cellSize {
buffer.WriteRune(' ')
buffer.WriteString(size.cssString(`auto`))
}
return buffer.String()
}
}
return ""
}
func (gridLayout *gridLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case VerticalAlign:
return CellVerticalAlign
case HorizontalAlign:
return CellHorizontalAlign
case "row-gap":
return GridRowGap
case ColumnGap:
return GridColumnGap
}
return tag
}
func (gridLayout *gridLayoutData) Get(tag string) interface{} {
return gridLayout.get(gridLayout.normalizeTag(tag))
}
func (gridLayout *gridLayoutData) get(tag string) interface{} {
if tag == Gap {
rowGap := GetGridRowGap(gridLayout, "")
columnGap := GetGridColumnGap(gridLayout, "")
if rowGap.Equal(columnGap) {
return rowGap
}
return AutoSize()
}
return gridLayout.viewsContainerData.get(tag)
}
func (gridLayout *gridLayoutData) Remove(tag string) {
gridLayout.remove(gridLayout.normalizeTag(tag))
}
func (gridLayout *gridLayoutData) remove(tag string) {
if tag == Gap {
gridLayout.remove(GridRowGap)
gridLayout.remove(GridColumnGap)
return
}
gridLayout.viewsContainerData.remove(tag)
switch tag {
case CellWidth:
updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
case CellHeight:
updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
}
}
func (gridLayout *gridLayoutData) Set(tag string, value interface{}) bool {
return gridLayout.set(gridLayout.normalizeTag(tag), value)
}
func (gridLayout *gridLayoutData) set(tag string, value interface{}) bool {
if value == nil {
gridLayout.remove(tag)
return true
}
if tag == Gap {
return gridLayout.set(GridRowGap, value) && gridLayout.set(GridColumnGap, value)
}
if gridLayout.viewsContainerData.set(tag, value) {
switch tag {
case CellWidth:
updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
case CellHeight:
updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
}
return true
}
return false
}
func gridCellSizes(properties Properties, tag string, session Session) []SizeUnit {
if value := properties.Get(tag); value != nil {
switch value := value.(type) {
case []SizeUnit:
return value
case SizeUnit:
return []SizeUnit{value}
case []interface{}:
result := make([]SizeUnit, len(value))
for i, val := range value {
result[i] = AutoSize()
switch val := val.(type) {
case SizeUnit:
result[i] = val
case string:
if text, ok := session.resolveConstants(val); ok {
result[i], _ = StringToSizeUnit(text)
}
}
}
return result
case string:
if text, ok := session.resolveConstants(value); ok {
values := strings.Split(text, ",")
result := make([]SizeUnit, len(values))
for i, val := range values {
result[i], _ = StringToSizeUnit(val)
}
return result
}
}
}
return []SizeUnit{}
}
func (gridLayout *gridLayoutData) cssStyle(self View, builder cssBuilder) {
gridLayout.viewsContainerData.cssStyle(self, builder)
// TODO
}
// GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3)
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetCellVerticalAlign(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if align, ok := enumProperty(view, CellVerticalAlign, view.Session(), StretchAlign); ok {
return align
}
}
return StretchAlign
}
// GetCellHorizontalAlign returns the vertical align of a GridLayout cell content: LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3)
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetCellHorizontalAlign(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if align, ok := enumProperty(view, CellHorizontalAlign, view.Session(), StretchAlign); ok {
return align
}
}
return StretchAlign
}
// GetCellWidth returns the width of a GridLayout cell. If the result is an empty array, then the width is not set.
// If the result is a single value array, then the width of all cell is equal.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetCellWidth(view View, subviewID string) []SizeUnit {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
return gridCellSizes(view, CellWidth, view.Session())
}
return []SizeUnit{}
}
// GetCellHeight returns the height of a GridLayout cell. If the result is an empty array, then the height is not set.
// If the result is a single value array, then the height of all cell is equal.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetCellHeight(view View, subviewID string) []SizeUnit {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
return gridCellSizes(view, CellHeight, view.Session())
}
return []SizeUnit{}
}
// GetGridRowGap returns the gap between GridLayout rows.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetGridRowGap(view View, subviewID string) SizeUnit {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := sizeProperty(view, GridRowGap, view.Session()); ok {
return result
}
}
return AutoSize()
}
// GetGridColumnGap returns the gap between GridLayout columns.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetGridColumnGap(view View, subviewID string) SizeUnit {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := sizeProperty(view, GridColumnGap, view.Session()); ok {
return result
}
}
return AutoSize()
}

132
image.go Normal file
View File

@ -0,0 +1,132 @@
package rui
import "strconv"
const (
// ImageLoading is the image loading status: in the process of loading
ImageLoading = 0
// ImageReady is the image loading status: the image is loaded successfully
ImageReady = 1
// ImageLoadingError is the image loading status: an error occurred while loading
ImageLoadingError = 2
)
// Image defines the image that is used for drawing operations on the Canvas.
type Image interface {
// URL returns the url of the image
URL() string
// LoadingStatus returns the status of the image loading: ImageLoading (0), ImageReady (1), ImageLoadingError (2)
LoadingStatus() int
// LoadingError: if LoadingStatus() == ImageLoadingError then returns the error text, "" otherwise
LoadingError() string
setLoadingError(err string)
// Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0
Width() float64
// Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0
Height() float64
}
type imageData struct {
url string
loadingStatus int
loadingError string
width, height float64
listener func(Image)
}
type imageManager struct {
images map[string]*imageData
}
func (image *imageData) URL() string {
return image.url
}
func (image *imageData) LoadingStatus() int {
return image.loadingStatus
}
func (image *imageData) LoadingError() string {
return image.loadingError
}
func (image *imageData) setLoadingError(err string) {
image.loadingError = err
}
func (image *imageData) Width() float64 {
return image.width
}
func (image *imageData) Height() float64 {
return image.height
}
func (manager *imageManager) loadImage(url string, onLoaded func(Image), session Session) Image {
if manager.images == nil {
manager.images = make(map[string]*imageData)
}
if image, ok := manager.images[url]; ok && image.loadingStatus == ImageReady {
return image
}
image := new(imageData)
image.url = url
image.listener = onLoaded
image.loadingStatus = ImageLoading
manager.images[url] = image
session.runScript("loadImage('" + url + "');")
return image
}
func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
if manager.images == nil {
manager.images = make(map[string]*imageData)
return
}
if url, ok := obj.PropertyValue("url"); ok {
if image, ok := manager.images[url]; ok {
image.loadingStatus = ImageReady
if width, ok := obj.PropertyValue("width"); ok {
if w, err := strconv.ParseFloat(width, 64); err == nil {
image.width = w
}
}
if height, ok := obj.PropertyValue("height"); ok {
if h, err := strconv.ParseFloat(height, 64); err == nil {
image.height = h
}
}
if image.listener != nil {
image.listener(image)
}
}
}
}
func (manager *imageManager) imageLoadError(obj DataObject, session Session) {
if manager.images == nil {
manager.images = make(map[string]*imageData)
return
}
if url, ok := obj.PropertyValue("url"); ok {
if image, ok := manager.images[url]; ok {
delete(manager.images, url)
text, _ := obj.PropertyValue("message")
image.setLoadingError(text)
if image.listener != nil {
image.listener(image)
}
}
}
}
// LoadImage starts the async image loading by url
func LoadImage(url string, onLoaded func(Image), session Session) Image {
return session.imageManager().loadImage(url, onLoaded, session)
}

264
imageView.go Normal file
View File

@ -0,0 +1,264 @@
package rui
import (
"fmt"
"strings"
)
const (
// NoneFit - value of the "object-fit" property of an ImageView. The replaced content is not resized
NoneFit = 0
// ContainFit - value of the "object-fit" property of an ImageView. The replaced content
// is scaled to maintain its aspect ratio while fitting within the elements content box.
// The entire object is made to fill the box, while preserving its aspect ratio, so the object
// will be "letterboxed" if its aspect ratio does not match the aspect ratio of the box.
ContainFit = 1
// CoverFit - value of the "object-fit" property of an ImageView. The replaced content
// is sized to maintain its aspect ratio while filling the elements entire content box.
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
CoverFit = 2
// FillFit - value of the "object-fit" property of an ImageView. The replaced content is sized
// to fill the elements content box. The entire object will completely fill the box.
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be stretched to fit.
FillFit = 3
// ScaleDownFit - value of the "object-fit" property of an ImageView. The content is sized as
// if NoneFit or ContainFit were specified, whichever would result in a smaller concrete object size.
ScaleDownFit = 4
)
// ImageView - image View
type ImageView interface {
View
}
type imageViewData struct {
viewData
}
// NewImageView create new ImageView object and return it
func NewImageView(session Session, params Params) ImageView {
view := new(imageViewData)
view.Init(session)
setInitParams(view, params)
return view
}
func newImageView(session Session) View {
return NewImageView(session, nil)
}
// Init initialize fields of imageView by default values
func (imageView *imageViewData) Init(session Session) {
imageView.viewData.Init(session)
imageView.tag = "ImageView"
//imageView.systemClass = "ruiImageView"
}
func (imageView *imageViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case "source":
tag = Source
case VerticalAlign:
tag = ImageVerticalAlign
case HorizontalAlign:
tag = ImageHorizontalAlign
case altProperty:
tag = AltText
}
return tag
}
func (imageView *imageViewData) Remove(tag string) {
imageView.remove(imageView.normalizeTag(tag))
}
func (imageView *imageViewData) remove(tag string) {
imageView.viewData.remove(tag)
switch tag {
case Source:
updateProperty(imageView.htmlID(), "src", "", imageView.session)
removeProperty(imageView.htmlID(), "srcset", imageView.session)
case AltText:
updateInnerHTML(imageView.htmlID(), imageView.session)
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session)
}
}
func (imageView *imageViewData) Set(tag string, value interface{}) bool {
return imageView.set(imageView.normalizeTag(tag), value)
}
func (imageView *imageViewData) set(tag string, value interface{}) bool {
if value == nil {
imageView.remove(tag)
return true
}
switch tag {
case Source:
if text, ok := value.(string); ok {
imageView.properties[Source] = text
updateProperty(imageView.htmlID(), "src", text, imageView.session)
if srcset := imageView.srcSet(text); srcset != "" {
updateProperty(imageView.htmlID(), "srcset", srcset, imageView.session)
} else {
removeProperty(imageView.htmlID(), "srcset", imageView.session)
}
return true
}
notCompatibleType(tag, value)
case AltText:
if text, ok := value.(string); ok {
imageView.properties[AltText] = text
updateInnerHTML(imageView.htmlID(), imageView.session)
return true
}
notCompatibleType(tag, value)
default:
if imageView.viewData.set(tag, value) {
switch tag {
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session)
}
return true
}
}
return false
}
func (imageView *imageViewData) Get(tag string) interface{} {
return imageView.viewData.get(imageView.normalizeTag(tag))
}
func (imageView *imageViewData) srcSet(path string) string {
if srcset, ok := resources.imageSrcSets[path]; ok {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for i, src := range srcset {
if i > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(src.path)
buffer.WriteString(fmt.Sprintf(" %gx", src.scale))
}
return buffer.String()
}
return ""
}
func (imageView *imageViewData) htmlTag() string {
return "img"
}
/*
func (imageView *imageViewData) closeHTMLTag() bool {
return false
}
*/
func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builder) {
imageView.viewData.htmlProperties(self, buffer)
imageResource := GetImageViewSource(imageView, "")
if imageResource != "" {
buffer.WriteString(` src="`)
buffer.WriteString(imageResource)
buffer.WriteString(`"`)
if srcset := imageView.srcSet(imageResource); srcset != "" {
buffer.WriteString(` srcset="`)
buffer.WriteString(srcset)
buffer.WriteString(`"`)
}
}
}
func (imageView *imageViewData) cssStyle(self View, builder cssBuilder) {
imageView.viewData.cssStyle(self, builder)
if value, ok := enumProperty(imageView, Fit, imageView.session, 0); ok {
builder.add("object-fit", enumProperties[Fit].cssValues[value])
} else {
builder.add("object-fit", "none")
}
vAlign := GetImageViewVerticalAlign(imageView, "")
hAlign := GetImageViewHorizontalAlign(imageView, "")
if vAlign != CenterAlign || hAlign != CenterAlign {
var position string
switch hAlign {
case LeftAlign:
position = "left"
case RightAlign:
position = "right"
default:
position = "center"
}
switch vAlign {
case TopAlign:
position += " top"
case BottomAlign:
position += " bottom"
default:
position += " center"
}
builder.add("object-position", position)
}
}
// GetImageViewSource returns the image URL of an ImageView subview.
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
func GetImageViewSource(view View, subviewID string) string {
if image, ok := stringProperty(view, Source, view.Session()); ok {
return image
}
return ""
}
// GetImageViewAltText returns an alternative text description of an ImageView subview.
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
func GetImageViewAltText(view View, subviewID string) string {
if text, ok := stringProperty(view, AltText, view.Session()); ok {
return text
}
return ""
}
// GetImageViewFit returns how the content of a replaced ImageView subview:
// NoneFit (0), ContainFit (1), CoverFit (2), FillFit (3), or ScaleDownFit (4).
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
func GetImageViewFit(view View, subviewID string) int {
if value, ok := enumProperty(view, Fit, view.Session(), 0); ok {
return value
}
return 0
}
// GetImageViewVerticalAlign return the vertical align of an ImageView subview: TopAlign (0), BottomAlign (1), CenterAlign (2)
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
func GetImageViewVerticalAlign(view View, subviewID string) int {
if align, ok := enumProperty(view, ImageVerticalAlign, view.Session(), LeftAlign); ok {
return align
}
return CenterAlign
}
// GetImageViewHorizontalAlign return the vertical align of an ImageView subview: LeftAlign (0), RightAlign (1), CenterAlign (2)
// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned
func GetImageViewHorizontalAlign(view View, subviewID string) int {
if align, ok := enumProperty(view, ImageHorizontalAlign, view.Session(), LeftAlign); ok {
return align
}
return CenterAlign
}

7
init.go Normal file
View File

@ -0,0 +1,7 @@
package rui
func init() {
//resources.init()
defaultTheme.init()
defaultTheme.addText(defaultThemeText)
}

271
keyEvents.go Normal file
View File

@ -0,0 +1,271 @@
package rui
import "strings"
const (
// KeyDown is the constant for "key-down-event" property tag.
// The "key-down-event" event is fired when a key is pressed.
// The main listener format: func(View, KeyEvent).
// The additional listener formats: func(KeyEvent), func(View), and func().
KeyDownEvent = "key-down-event"
// KeyPp is the constant for "key-up-event" property tag
// The "key-up-event" event is fired when a key is released.
// The main listener format: func(View, KeyEvent).
// The additional listener formats: func(KeyEvent), func(View), and func().
KeyUpEvent = "key-up-event"
)
type KeyEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary.
TimeStamp uint64
// Key is the key value of the key represented by the event. If the value has a printed representation,
// this attribute's value is the same as the char property. Otherwise, it's one of the key value strings
// specified in Key values. If the key can't be identified, its value is the string "Unidentified".
Key string
// Code holds a string that identifies the physical key being pressed. The value is not affected
// by the current keyboard layout or modifier state, so a particular key will always return the same value.
Code string
// Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false.
Repeat bool
// CtrlKey == true if the control key was down when the event was fired. false otherwise.
CtrlKey bool
// ShiftKey == true if the shift key was down when the event was fired. false otherwise.
ShiftKey bool
// AltKey == true if the alt key was down when the event was fired. false otherwise.
AltKey bool
// MetaKey == true if the meta key was down when the event was fired. false otherwise.
MetaKey bool
}
func valueToKeyListeners(value interface{}) ([]func(View, KeyEvent), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(View, KeyEvent):
return []func(View, KeyEvent){value}, true
case func(KeyEvent):
fn := func(view View, event KeyEvent) {
value(event)
}
return []func(View, KeyEvent){fn}, true
case func(View):
fn := func(view View, event KeyEvent) {
value(view)
}
return []func(View, KeyEvent){fn}, true
case func():
fn := func(view View, event KeyEvent) {
value()
}
return []func(View, KeyEvent){fn}, true
case []func(View, KeyEvent):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(KeyEvent):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, KeyEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event KeyEvent) {
v(event)
}
}
return listeners, true
case []func(View):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, KeyEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event KeyEvent) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, KeyEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event KeyEvent) {
v()
}
}
return listeners, true
case []interface{}:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, KeyEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(View, KeyEvent):
listeners[i] = v
case func(KeyEvent):
listeners[i] = func(view View, event KeyEvent) {
v(event)
}
case func(View):
listeners[i] = func(view View, event KeyEvent) {
v(view)
}
case func():
listeners[i] = func(view View, event KeyEvent) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var keyEvents = map[string]struct{ jsEvent, jsFunc string }{
KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"},
KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"},
}
func (view *viewData) setKeyListener(tag string, value interface{}) bool {
listeners, ok := valueToKeyListeners(value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeKeyListener(tag)
} else if js, ok := keyEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeKeyListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := keyEvents[tag]; ok {
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
}
}
}
func getKeyListeners(view View, subviewID string, tag string) []func(View, KeyEvent) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(View, KeyEvent)); ok {
return result
}
}
}
return []func(View, KeyEvent){}
}
func keyEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range keyEvents {
if listeners := getKeyListeners(view, "", tag); len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
func handleKeyEvents(view View, tag string, data DataObject) {
listeners := getKeyListeners(view, "", tag)
if len(listeners) == 0 {
return
}
getBool := func(tag string) bool {
if value, ok := data.PropertyValue(tag); ok && value == "1" {
return true
}
return false
}
key, _ := data.PropertyValue("key")
code, _ := data.PropertyValue("code")
event := KeyEvent{
TimeStamp: getTimeStamp(data),
Key: key,
Code: code,
Repeat: getBool("repeat"),
CtrlKey: getBool("ctrlKey"),
ShiftKey: getBool("shiftKey"),
AltKey: getBool("altKey"),
MetaKey: getBool("metaKey"),
}
for _, listener := range listeners {
listener(view, event)
}
}
// GetKeyDownListeners returns the "key-down-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetKeyDownListeners(view View, subviewID string) []func(View, KeyEvent) {
return getKeyListeners(view, subviewID, KeyDownEvent)
}
// GetKeyUpListeners returns the "key-up-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetKeyUpListeners(view View, subviewID string) []func(View, KeyEvent) {
return getKeyListeners(view, subviewID, KeyUpEvent)
}

83
listAdapter.go Normal file
View File

@ -0,0 +1,83 @@
package rui
// ListAdapter - the list data source
type ListAdapter interface {
ListSize() int
ListItem(index int, session Session) View
IsListItemEnabled(index int) bool
}
type textListAdapter struct {
items []string
views []View
params Params
}
type viewListAdapter struct {
items []View
}
// NewTextListAdapter create the new ListAdapter for a string list displaying. The second argument is parameters of a TextView item
func NewTextListAdapter(items []string, params Params) ListAdapter {
if items == nil {
return nil
}
adapter := new(textListAdapter)
adapter.items = items
if params != nil {
adapter.params = params
} else {
adapter.params = Params{}
}
adapter.views = make([]View, len(items))
return adapter
}
// NewTextListAdapter create the new ListAdapter for a view list displaying
func NewViewListAdapter(items []View) ListAdapter {
if items != nil {
adapter := new(viewListAdapter)
adapter.items = items
return adapter
}
return nil
}
func (adapter *textListAdapter) ListSize() int {
return len(adapter.items)
}
func (adapter *textListAdapter) ListItem(index int, session Session) View {
if index < 0 || index >= len(adapter.items) {
return nil
}
if adapter.views[index] == nil {
adapter.params[Text] = adapter.items[index]
adapter.views[index] = NewTextView(session, adapter.params)
}
return adapter.views[index]
}
func (adapter *textListAdapter) IsListItemEnabled(index int) bool {
return true
}
func (adapter *viewListAdapter) ListSize() int {
return len(adapter.items)
}
func (adapter *viewListAdapter) ListItem(index int, session Session) View {
if index >= 0 && index < len(adapter.items) {
return adapter.items[index]
}
return nil
}
func (adapter *viewListAdapter) IsListItemEnabled(index int) bool {
if index >= 0 && index < len(adapter.items) {
return !IsDisabled(adapter.items[index])
}
return true
}

148
listLayout.go Normal file
View File

@ -0,0 +1,148 @@
package rui
import (
"strings"
)
const (
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
TopDownOrientation = 0
// StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation
StartToEndOrientation = 1
// BottomUpOrientation - subviews are arranged from bottom to top
BottomUpOrientation = 2
// EndToStartOrientation - subviews are arranged from right to left
EndToStartOrientation = 3
// WrapOff - subviews are scrolled and "true" if a new row/column starts
WrapOff = 0
// WrapOn - the new row/column starts at bottom/right
WrapOn = 1
// WrapReverse - the new row/column starts at top/left
WrapReverse = 2
)
// ListLayout - list-container of View
type ListLayout interface {
ViewsContainer
}
type listLayoutData struct {
viewsContainerData
}
// NewListLayout create new ListLayout object and return it
func NewListLayout(session Session, params Params) ListLayout {
view := new(listLayoutData)
view.Init(session)
setInitParams(view, params)
return view
}
func newListLayout(session Session) View {
return NewListLayout(session, nil)
}
// Init initialize fields of ViewsAlignContainer by default values
func (listLayout *listLayoutData) Init(session Session) {
listLayout.viewsContainerData.Init(session)
listLayout.tag = "ListLayout"
listLayout.systemClass = "ruiListLayout"
}
func (listLayout *listLayoutData) Remove(tag string) {
listLayout.remove(strings.ToLower(tag))
}
func (listLayout *listLayoutData) remove(tag string) {
listLayout.viewsContainerData.remove(tag)
switch tag {
case Orientation, Wrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.session)
}
}
func (listLayout *listLayoutData) Set(tag string, value interface{}) bool {
return listLayout.set(strings.ToLower(tag), value)
}
func (listLayout *listLayoutData) set(tag string, value interface{}) bool {
if value == nil {
listLayout.remove(tag)
return true
}
if listLayout.viewsContainerData.set(tag, value) {
switch tag {
case Orientation, Wrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.session)
}
return true
}
return false
}
func (listLayout *listLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
if listLayout.views != nil {
for _, view := range listLayout.views {
view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`})
viewHTML(view, buffer)
}
}
}
// GetListVerticalAlign returns the vertical align of a ListLayout or ListView sibview:
// TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3)
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetListVerticalAlign(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return LeftAlign
}
result, _ := enumProperty(view, VerticalAlign, view.Session(), 0)
return result
}
// GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview:
// LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3)
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetListHorizontalAlign(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return TopAlign
}
result, _ := enumProperty(view, HorizontalAlign, view.Session(), 0)
return result
}
// GetListOrientation returns the orientation of a ListLayout or ListView subview:
// TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3)
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetListOrientation(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return 0
}
orientation, _ := getOrientation(view, view.Session())
return orientation
}
// GetListWrap returns the wrap type of a ListLayout or ListView subview:
// WrapOff (0), WrapOn (1), or WrapReverse (2)
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetListWrap(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := enumProperty(view, Wrap, view.Session(), 0); ok {
return result
}
}
return WrapOff
}

1278
listView.go Normal file

File diff suppressed because it is too large Load Diff

1162
mediaPlayer.go Normal file

File diff suppressed because it is too large Load Diff

406
mouseEvents.go Normal file
View File

@ -0,0 +1,406 @@
package rui
import (
"strconv"
"strings"
)
const (
// ClickEvent is the constant for "click-event" property tag
// The "click-event" event occurs when the user clicks on the View.
// The main listener format: func(View, MouseEvent).
// The additional listener formats: func(MouseEvent), func(View), and func().
ClickEvent = "click-event"
// DoubleClickEvent is the constant for "double-click-event" property tag
// The "double-click-event" event occurs when the user double clicks on the View.
// The main listener format: func(View, MouseEvent).
// The additional listener formats: func(MouseEvent), func(View), and func().
DoubleClickEvent = "double-click-event"
// MouseDown is the constant for "mouse-down" property tag.
// The "mouse-down" event is fired at a View when a pointing device button is pressed
// while the pointer is inside the view.
// The main listener format: func(View, MouseEvent).
// The additional listener formats: func(MouseEvent), func(View), and func().
MouseDown = "mouse-down"
// MouseUp is the constant for "mouse-up" property tag.
// The "mouse-up" event is fired at a View when a button on a pointing device (such as a mouse
// or trackpad) is released while the pointer is located inside it.
// "mouse-up" events are the counterpoint to "mouse-down" events.
// The main listener format: func(View, MouseEvent).
// The additional listener formats: func(MouseEvent), func(View), and func().
MouseUp = "mouse-up"
// MouseMove is the constant for "mouse-move" property tag.
// The "mouse-move" event is fired at a view when a pointing device (usually a mouse) is moved
// while the cursor's hotspot is inside it.
// The main listener format: func(View, MouseEvent).
// The additional listener formats: func(MouseEvent), func(View), and func().
MouseMove = "mouse-move"
// MouseOut is the constant for "mouse-out" property tag.
// The "mouse-out" event is fired at a View when a pointing device (usually a mouse) is used to move
// the cursor so that it is no longer contained within the view or one of its children.
// "mouse-out" is also delivered to a view if the cursor enters a child view,
// because the child view obscures the visible area of the view.
// The main listener format: func(View, MouseEvent).
// The additional listener formats: func(MouseEvent), func(View), and func().
MouseOut = "mouse-out"
// MouseOver is the constant for "mouse-over" property tag.
// The "mouse-over" event is fired at a View when a pointing device (such as a mouse or trackpad)
// is used to move the cursor onto the view or one of its child views.
// The main listener formats: func(View, MouseEvent).
MouseOver = "mouse-over"
// ContextMenuEvent is the constant for "context-menu-event" property tag
// The "context-menu-event" event occurs when the user calls the context menu by the right mouse clicking.
// The main listener format: func(View, MouseEvent).
ContextMenuEvent = "context-menu-event"
// PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state
PrimaryMouseButton = 0
// AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button
// or the middle button (if present)
AuxiliaryMouseButton = 1
// SecondaryMouseButton is a number of the secondary pressed button, usually the right button
SecondaryMouseButton = 2
// MouseButton4 is a number of the fourth button, typically the Browser Back button
MouseButton4 = 3
// MouseButton5 is a number of the fifth button, typically the Browser Forward button
MouseButton5 = 4
// PrimaryMouseMask is the mask of the primary button (usually the left button)
PrimaryMouseMask = 1
// SecondaryMouseMask is the mask of the secondary button (usually the right button)
SecondaryMouseMask = 2
// AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button)
AuxiliaryMouseMask = 4
// MouseMask4 is the mask of the 4th button (typically the "Browser Back" button)
MouseMask4 = 8
//MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button)
MouseMask5 = 16
)
type MouseEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary.
TimeStamp uint64
// Button indicates which button was pressed on the mouse to trigger the event:
// PrimaryMouseButton (0), AuxiliaryMouseButton (1), SecondaryMouseButton (2),
// MouseButton4 (3), and MouseButton5 (4)
Button int
// Buttons indicates which buttons are pressed on the mouse (or other input device)
// when a mouse event is triggered. Each button that can be pressed is represented by a given mask:
// PrimaryMouseMask (1), SecondaryMouseMask (2), AuxiliaryMouseMask (4), MouseMask4 (8), and MouseMask5 (16)
Buttons int
// X provides the horizontal coordinate within the view's viewport.
X float64
// Y provides the vertical coordinate within the view's viewport.
Y float64
// ClientX provides the horizontal coordinate within the application's viewport at which the event occurred.
ClientX float64
// ClientY provides the vertical coordinate within the application's viewport at which the event occurred.
ClientY float64
// ScreenX provides the horizontal coordinate (offset) of the mouse pointer in global (screen) coordinates.
ScreenX float64
// ScreenY provides the vertical coordinate (offset) of the mouse pointer in global (screen) coordinates.
ScreenY float64
// CtrlKey == true if the control key was down when the event was fired. false otherwise.
CtrlKey bool
// ShiftKey == true if the shift key was down when the event was fired. false otherwise.
ShiftKey bool
// AltKey == true if the alt key was down when the event was fired. false otherwise.
AltKey bool
// MetaKey == true if the meta key was down when the event was fired. false otherwise.
MetaKey bool
}
func valueToMouseListeners(value interface{}) ([]func(View, MouseEvent), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(View, MouseEvent):
return []func(View, MouseEvent){value}, true
case func(MouseEvent):
fn := func(view View, event MouseEvent) {
value(event)
}
return []func(View, MouseEvent){fn}, true
case func(View):
fn := func(view View, event MouseEvent) {
value(view)
}
return []func(View, MouseEvent){fn}, true
case func():
fn := func(view View, event MouseEvent) {
value()
}
return []func(View, MouseEvent){fn}, true
case []func(View, MouseEvent):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(MouseEvent):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, MouseEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event MouseEvent) {
v(event)
}
}
return listeners, true
case []func(View):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, MouseEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event MouseEvent) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, MouseEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event MouseEvent) {
v()
}
}
return listeners, true
case []interface{}:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, MouseEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(View, MouseEvent):
listeners[i] = v
case func(MouseEvent):
listeners[i] = func(view View, event MouseEvent) {
v(event)
}
case func(View):
listeners[i] = func(view View, event MouseEvent) {
v(view)
}
case func():
listeners[i] = func(view View, event MouseEvent) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var mouseEvents = map[string]struct{ jsEvent, jsFunc string }{
ClickEvent: {jsEvent: "onclick", jsFunc: "clickEvent"},
DoubleClickEvent: {jsEvent: "ondblclick", jsFunc: "doubleClickEvent"},
MouseDown: {jsEvent: "onmousedown", jsFunc: "mouseDownEvent"},
MouseUp: {jsEvent: "onmouseup", jsFunc: "mouseUpEvent"},
MouseMove: {jsEvent: "onmousemove", jsFunc: "mouseMoveEvent"},
MouseOut: {jsEvent: "onmouseout", jsFunc: "mouseOutEvent"},
MouseOver: {jsEvent: "onmouseover", jsFunc: "mouseOverEvent"},
ContextMenuEvent: {jsEvent: "oncontextmenu", jsFunc: "contextMenuEvent"},
}
func (view *viewData) setMouseListener(tag string, value interface{}) bool {
listeners, ok := valueToMouseListeners(value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeMouseListener(tag)
} else if js, ok := mouseEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeMouseListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := mouseEvents[tag]; ok {
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
}
}
}
func getMouseListeners(view View, subviewID string, tag string) []func(View, MouseEvent) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(View, MouseEvent)); ok {
return result
}
}
}
return []func(View, MouseEvent){}
}
func mouseEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range mouseEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, MouseEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func getTimeStamp(data DataObject) uint64 {
if value, ok := data.PropertyValue("timeStamp"); ok {
if index := strings.Index(value, "."); index > 0 {
value = value[:index]
}
if n, err := strconv.ParseUint(value, 10, 64); err == nil {
return n
}
}
return 0
}
func (event *MouseEvent) init(data DataObject) {
event.TimeStamp = getTimeStamp(data)
event.Button = dataIntProperty(data, "button")
event.Buttons = dataIntProperty(data, "buttons")
event.X = dataFloatProperty(data, "x")
event.Y = dataFloatProperty(data, "y")
event.ClientX = dataFloatProperty(data, "clientX")
event.ClientY = dataFloatProperty(data, "clientY")
event.ScreenX = dataFloatProperty(data, "screenX")
event.ScreenY = dataFloatProperty(data, "screenY")
event.CtrlKey = dataBoolProperty(data, "ctrlKey")
event.ShiftKey = dataBoolProperty(data, "shiftKey")
event.AltKey = dataBoolProperty(data, "altKey")
event.MetaKey = dataBoolProperty(data, "metaKey")
}
func handleMouseEvents(view View, tag string, data DataObject) {
listeners := getMouseListeners(view, "", tag)
if len(listeners) == 0 {
return
}
var event MouseEvent
event.init(data)
for _, listener := range listeners {
listener(view, event)
}
}
// GetClickListeners returns the "click-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetClickListeners(view View, subviewID string) []func(View, MouseEvent) {
return getMouseListeners(view, subviewID, ClickEvent)
}
// GetDoubleClickListeners returns the "double-click-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent) {
return getMouseListeners(view, subviewID, DoubleClickEvent)
}
// GetContextMenuListeners returns the "context-menu" listener list.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent) {
return getMouseListeners(view, subviewID, ContextMenuEvent)
}
// GetMouseDownListeners returns the "mouse-down" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetMouseDownListeners(view View, subviewID string) []func(View, MouseEvent) {
return getMouseListeners(view, subviewID, MouseDown)
}
// GetMouseUpListeners returns the "mouse-up" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetMouseUpListeners(view View, subviewID string) []func(View, MouseEvent) {
return getMouseListeners(view, subviewID, MouseUp)
}
// GetMouseMoveListeners returns the "mouse-move" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetMouseMoveListeners(view View, subviewID string) []func(View, MouseEvent) {
return getMouseListeners(view, subviewID, MouseMove)
}
// GetMouseOverListeners returns the "mouse-over" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetMouseOverListeners(view View, subviewID string) []func(View, MouseEvent) {
return getMouseListeners(view, subviewID, MouseOver)
}
// GetMouseOutListeners returns the "mouse-out" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetMouseOutListeners(view View, subviewID string) []func(View, MouseEvent) {
return getMouseListeners(view, subviewID, MouseOut)
}

371
numberPicker.go Normal file
View File

@ -0,0 +1,371 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
const (
NumberChangedEvent = "number-changed"
NumberPickerType = "number-picker-type"
NumberPickerMin = "number-picker-min"
NumberPickerMax = "number-picker-max"
NumberPickerStep = "number-picker-step"
NumberPickerValue = "number-picker-value"
)
const (
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor
NumberEditor = 0
// NumberSlider - type of NumberPicker. NumberPicker is presented by slider
NumberSlider = 1
)
// NumberPicker - NumberPicker view
type NumberPicker interface {
View
}
type numberPickerData struct {
viewData
numberChangedListeners []func(NumberPicker, float64)
}
// NewNumberPicker create new NumberPicker object and return it
func NewNumberPicker(session Session, params Params) NumberPicker {
view := new(numberPickerData)
view.Init(session)
setInitParams(view, params)
return view
}
func newNumberPicker(session Session) View {
return NewNumberPicker(session, nil)
}
func (picker *numberPickerData) Init(session Session) {
picker.viewData.Init(session)
picker.tag = "NumberPicker"
picker.numberChangedListeners = []func(NumberPicker, float64){}
}
func (picker *numberPickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case Type, Min, Max, Step, Value:
return "number-picker-" + tag
}
return tag
}
func (picker *numberPickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *numberPickerData) remove(tag string) {
switch tag {
case NumberChangedEvent:
if len(picker.numberChangedListeners) > 0 {
picker.numberChangedListeners = []func(NumberPicker, float64){}
}
default:
picker.viewData.remove(tag)
picker.propertyChanged(tag)
}
}
func (picker *numberPickerData) Set(tag string, value interface{}) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *numberPickerData) set(tag string, value interface{}) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag {
case NumberChangedEvent:
switch value := value.(type) {
case func(NumberPicker, float64):
picker.numberChangedListeners = []func(NumberPicker, float64){value}
case func(float64):
fn := func(view NumberPicker, newValue float64) {
value(newValue)
}
picker.numberChangedListeners = []func(NumberPicker, float64){fn}
case []func(NumberPicker, float64):
picker.numberChangedListeners = value
case []func(float64):
listeners := make([]func(NumberPicker, float64), len(value))
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
listeners[i] = func(view NumberPicker, newValue float64) {
val(newValue)
}
}
picker.numberChangedListeners = listeners
case []interface{}:
listeners := make([]func(NumberPicker, float64), len(value))
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
switch val := val.(type) {
case func(NumberPicker, float64):
listeners[i] = val
default:
notCompatibleType(tag, val)
return false
}
}
picker.numberChangedListeners = listeners
}
return true
case NumberPickerValue:
oldValue := GetNumberPickerValue(picker, "")
min, max := GetNumberPickerMinMax(picker, "")
if picker.setFloatProperty(NumberPickerValue, value, min, max) {
newValue := GetNumberPickerValue(picker, "")
if oldValue != newValue {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), newValue))
for _, listener := range picker.numberChangedListeners {
listener(picker, newValue)
}
}
return true
}
default:
if picker.viewData.set(tag, value) {
picker.propertyChanged(tag)
return true
}
}
return false
}
func (picker *numberPickerData) propertyChanged(tag string) {
switch tag {
case NumberPickerType:
if GetNumberPickerType(picker, "") == NumberSlider {
updateProperty(picker.htmlID(), "type", "range", picker.session)
} else {
updateProperty(picker.htmlID(), "type", "number", picker.session)
}
case NumberPickerMin:
min, _ := GetNumberPickerMinMax(picker, "")
updateProperty(picker.htmlID(), Min, strconv.FormatFloat(min, 'f', -1, 32), picker.session)
case NumberPickerMax:
_, max := GetNumberPickerMinMax(picker, "")
updateProperty(picker.htmlID(), Max, strconv.FormatFloat(max, 'f', -1, 32), picker.session)
case NumberPickerStep:
if step := GetNumberPickerStep(picker, ""); step > 0 {
updateProperty(picker.htmlID(), Step, strconv.FormatFloat(step, 'f', -1, 32), picker.session)
} else {
updateProperty(picker.htmlID(), Step, "any", picker.session)
}
case NumberPickerValue:
value := GetNumberPickerValue(picker, "")
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), value))
for _, listener := range picker.numberChangedListeners {
listener(picker, value)
}
}
}
func (picker *numberPickerData) Get(tag string) interface{} {
return picker.get(picker.normalizeTag(tag))
}
func (picker *numberPickerData) get(tag string) interface{} {
switch tag {
case NumberChangedEvent:
return picker.numberChangedListeners
default:
return picker.viewData.get(tag)
}
}
func (picker *numberPickerData) htmlTag() string {
return "input"
}
func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer)
if GetNumberPickerType(picker, "") == NumberSlider {
buffer.WriteString(` type="range"`)
} else {
buffer.WriteString(` type="number"`)
}
min, max := GetNumberPickerMinMax(picker, "")
buffer.WriteString(` min="`)
buffer.WriteString(strconv.FormatFloat(min, 'f', -1, 64))
buffer.WriteByte('"')
buffer.WriteString(` max="`)
buffer.WriteString(strconv.FormatFloat(max, 'f', -1, 64))
buffer.WriteByte('"')
step := GetNumberPickerStep(picker, "")
if step != 0 {
buffer.WriteString(` step="`)
buffer.WriteString(strconv.FormatFloat(step, 'f', -1, 64))
buffer.WriteByte('"')
} else {
buffer.WriteString(` step="any"`)
}
buffer.WriteString(` value="`)
buffer.WriteString(strconv.FormatFloat(GetNumberPickerValue(picker, ""), 'f', -1, 64))
buffer.WriteByte('"')
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
}
func (picker *numberPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
if value, err := strconv.ParseFloat(text, 32); err == nil {
oldValue := GetNumberPickerValue(picker, "")
picker.properties[NumberPickerValue] = value
if value != oldValue {
for _, listener := range picker.numberChangedListeners {
listener(picker, value)
}
}
}
}
return true
}
return picker.viewData.handleCommand(self, command, data)
}
// GetNumberPickerType returns the type of NumberPicker subview. Valid values:
// NumberEditor (0) - NumberPicker is presented by editor (default type)
// NumberSlider (1) - NumberPicker is presented by slider
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetNumberPickerType(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return 0
}
t, _ := enumStyledProperty(view, NumberPickerType, NumberEditor)
return t
}
// GetNumberPickerMinMax returns the min and max value of NumberPicker subview.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetNumberPickerMinMax(view View, subviewID string) (float64, float64) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
min, ok := floatStyledProperty(view, NumberPickerMin, 0)
if !ok {
min, _ = floatStyledProperty(view, Min, 0)
}
max, ok := floatStyledProperty(view, NumberPickerMax, 1)
if !ok {
min, _ = floatStyledProperty(view, Max, 1)
}
if min > max {
return max, min
}
return min, max
}
return 0, 1
}
// GetNumberPickerStep returns the value changing step of NumberPicker subview.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetNumberPickerStep(view View, subviewID string) float64 {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return 0
}
result, ok := floatStyledProperty(view, NumberPickerStep, 0)
if !ok {
result, _ = floatStyledProperty(view, Step, 0)
}
_, max := GetNumberPickerMinMax(view, "")
if result > max {
return max
}
return result
}
// GetNumberPickerValue returns the value of NumberPicker subview.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetNumberPickerValue(view View, subviewID string) float64 {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return 0
}
min, _ := GetNumberPickerMinMax(view, "")
result, ok := floatStyledProperty(view, NumberPickerValue, min)
if !ok {
result, _ = floatStyledProperty(view, Value, min)
}
return result
}
// GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview.
// If there are no listeners then the empty list is returned
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetNumberChangedListeners(view View, subviewID string) []func(NumberPicker, float64) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(NumberChangedEvent); value != nil {
if listeners, ok := value.([]func(NumberPicker, float64)); ok {
return listeners
}
}
}
return []func(NumberPicker, float64){}
}

153
outline.go Normal file
View File

@ -0,0 +1,153 @@
package rui
import (
"fmt"
"strings"
)
type OutlineProperty interface {
Properties
ruiStringer
fmt.Stringer
ViewOutline(session Session) ViewOutline
}
type outlinePropertyData struct {
propertyList
}
func NewOutlineProperty(params Params) OutlineProperty {
outline := new(outlinePropertyData)
outline.properties = map[string]interface{}{}
for tag, value := range params {
outline.Set(tag, value)
}
return outline
}
func (outline *outlinePropertyData) ruiString(writer ruiWriter) {
writer.startObject("_")
for _, tag := range []string{Style, Width, ColorProperty} {
if value, ok := outline.properties[tag]; ok {
writer.writeProperty(Style, value)
}
}
writer.endObject()
}
func (outline *outlinePropertyData) String() string {
writer := newRUIWriter()
outline.ruiString(writer)
return writer.finish()
}
func (outline *outlinePropertyData) normalizeTag(tag string) string {
return strings.TrimPrefix(strings.ToLower(tag), "outline-")
}
func (outline *outlinePropertyData) Remove(tag string) {
delete(outline.properties, outline.normalizeTag(tag))
}
func (outline *outlinePropertyData) Set(tag string, value interface{}) bool {
if value == nil {
outline.Remove(tag)
return true
}
tag = outline.normalizeTag(tag)
switch tag {
case Style:
return outline.setEnumProperty(Style, value, enumProperties[BorderStyle].values)
case Width:
if width, ok := value.(SizeUnit); ok {
switch width.Type {
case SizeInFraction, SizeInPercent:
notCompatibleType(tag, value)
return false
}
}
return outline.setSizeProperty(Width, value)
case ColorProperty:
return outline.setColorProperty(ColorProperty, value)
default:
ErrorLogF(`"%s" property is not compatible with the OutlineProperty`, tag)
}
return false
}
func (outline *outlinePropertyData) Get(tag string) interface{} {
return outline.propertyList.Get(outline.normalizeTag(tag))
}
func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline {
style, _ := valueToEnum(outline.getRaw(Style), BorderStyle, session, NoneLine)
width, _ := sizeProperty(outline, Width, session)
color, _ := colorProperty(outline, ColorProperty, session)
return ViewOutline{Style: style, Width: width, Color: color}
}
// ViewOutline describes parameters of a view border
type ViewOutline struct {
Style int
Color Color
Width SizeUnit
}
func (outline ViewOutline) cssValue(builder cssBuilder) {
values := enumProperties[BorderStyle].cssValues
if outline.Style > 0 && outline.Style < len(values) && outline.Color.Alpha() > 0 &&
outline.Width.Type != Auto && outline.Width.Type != SizeInFraction &&
outline.Width.Type != SizeInPercent && outline.Width.Value > 0 {
builder.addValues("outline", " ", outline.Width.cssString("0"), values[outline.Style], outline.Color.cssString())
}
}
func (outline ViewOutline) cssString() string {
var builder cssValueBuilder
outline.cssValue(&builder)
return builder.finish()
}
func getOutline(properties Properties) OutlineProperty {
if value := properties.Get(Outline); value != nil {
if outline, ok := value.(OutlineProperty); ok {
return outline
}
}
return nil
}
func (style *viewStyle) setOutline(value interface{}) bool {
switch value := value.(type) {
case OutlineProperty:
style.properties[Outline] = value
case ViewOutline:
style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorProperty: value.Color})
case ViewBorder:
style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorProperty: value.Color})
case DataObject:
outline := NewOutlineProperty(nil)
for _, tag := range []string{Style, Width, ColorProperty} {
if text, ok := value.PropertyValue(tag); ok && text != "" {
outline.Set(tag, text)
}
}
style.properties[Outline] = outline
default:
notCompatibleType(Outline, value)
return false
}
return true
}

196
path.go Normal file
View File

@ -0,0 +1,196 @@
package rui
import (
"strconv"
"strings"
)
// Path is a path interface
type Path interface {
// Reset erases the Path
Reset()
// MoveTo begins a new sub-path at the point specified by the given (x, y) coordinates
MoveTo(x, y float64)
// LineTo adds a straight line to the current sub-path by connecting
// the sub-path's last point to the specified (x, y) coordinates
LineTo(x, y float64)
// ArcTo adds a circular arc to the current sub-path, using the given control points and radius.
// The arc is automatically connected to the path's latest point with a straight line, if necessary.
// x0, y0 - coordinates of the first control point;
// x1, y1 - coordinates of the second control point;
// radius - the arc's radius. Must be non-negative.
ArcTo(x0, y0, x1, y1, radius float64)
// Arc adds a circular arc to the current sub-path.
// x, y - coordinates of the arc's center;
// radius - the arc's radius. Must be non-negative;
// startAngle - the angle at which the arc starts, measured clockwise from the positive
// x-axis and expressed in radians.
// endAngle - the angle at which the arc ends, measured clockwise from the positive
// x-axis and expressed in radians.
// clockwise - if true, causes the arc to be drawn clockwise between the start and end angles,
// otherwise - counter-clockwise
Arc(x, y, radius, startAngle, endAngle float64, clockwise bool)
// BezierCurveTo adds a cubic Bézier curve to the current sub-path. The starting point is
// the latest point in the current path.
// cp0x, cp0y - coordinates of the first control point;
// cp1x, cp1y - coordinates of the second control point;
// x, y - coordinates of the end point.
BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64)
// QuadraticCurveTo adds a quadratic Bézier curve to the current sub-path.
// cpx, cpy - coordinates of the control point;
// x, y - coordinates of the end point.
QuadraticCurveTo(cpx, cpy, x, y float64)
// Ellipse adds an elliptical arc to the current sub-path
// x, y - coordinates of the ellipse's center;
// radiusX - the ellipse's major-axis radius. Must be non-negative;
// radiusY - the ellipse's minor-axis radius. Must be non-negative;
// rotation - the rotation of the ellipse, expressed in radians;
// startAngle - the angle at which the ellipse starts, measured clockwise
// from the positive x-axis and expressed in radians;
// endAngle - the angle at which the ellipse ends, measured clockwise
// from the positive x-axis and expressed in radians.
// clockwise - if true, draws the ellipse clockwise, otherwise draws counter-clockwise
Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool)
// Close adds a straight line from the current point to the start of the current sub-path.
// If the shape has already been closed or has only one point, this function does nothing.
Close()
scriptText() string
}
type pathData struct {
script strings.Builder
}
// NewPath creates a new empty Path
func NewPath() Path {
path := new(pathData)
path.script.Grow(4096)
path.script.WriteString("\nctx.beginPath();")
return path
}
func (path *pathData) Reset() {
path.script.Reset()
path.script.WriteString("\nctx.beginPath();")
}
func (path *pathData) MoveTo(x, y float64) {
path.script.WriteString("\nctx.moveTo(")
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
}
func (path *pathData) LineTo(x, y float64) {
path.script.WriteString("\nctx.lineTo(")
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
}
func (path *pathData) ArcTo(x0, y0, x1, y1, radius float64) {
if radius > 0 {
path.script.WriteString("\nctx.arcTo(")
path.script.WriteString(strconv.FormatFloat(x0, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y0, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(x1, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y1, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(radius, 'g', -1, 64))
path.script.WriteString(");")
}
}
func (path *pathData) Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) {
if radius > 0 {
path.script.WriteString("\nctx.arc(")
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(radius, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(startAngle, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(endAngle, 'g', -1, 64))
if !clockwise {
path.script.WriteString(",true);")
} else {
path.script.WriteString(");")
}
}
}
func (path *pathData) BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) {
path.script.WriteString("\nctx.bezierCurveTo(")
path.script.WriteString(strconv.FormatFloat(cp0x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(cp0y, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(cp1x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(cp1y, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
}
func (path *pathData) QuadraticCurveTo(cpx, cpy, x, y float64) {
path.script.WriteString("\nctx.quadraticCurveTo(")
path.script.WriteString(strconv.FormatFloat(cpx, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(cpy, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
}
func (path *pathData) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) {
if radiusX > 0 && radiusY > 0 {
path.script.WriteString("\nctx.ellipse(")
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(radiusX, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(radiusY, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(rotation, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(startAngle, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(endAngle, 'g', -1, 64))
if !clockwise {
path.script.WriteString(",true);")
} else {
path.script.WriteString(");")
}
}
}
func (path *pathData) Close() {
path.script.WriteString("\nctx.close();")
}
func (path *pathData) scriptText() string {
return path.script.String()
}

341
pointerEvents.go Normal file
View File

@ -0,0 +1,341 @@
package rui
import (
"strings"
)
const (
// PointerDown is the constant for "pointer-down" property tag.
// The "pointer-down" event is fired when a pointer becomes active. For mouse, it is fired when
// the device transitions from no buttons depressed to at least one button depressed.
// For touch, it is fired when physical contact is made with the digitizer.
// For pen, it is fired when the stylus makes physical contact with the digitizer.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerDown = "pointer-down"
// PointerUp is the constant for "pointer-up" property tag.
// The "pointer-up" event is fired when a pointer is no longer active.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerUp = "pointer-up"
// PointerMove is the constant for "pointer-move" property tag.
// The "pointer-move" event is fired when a pointer changes coordinates.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerMove = "pointer-move"
// PointerCancel is the constant for "pointer-cancel" property tag.
// The "pointer-cancel" event is fired if the pointer will no longer be able to generate events
// (for example the related device is deactivated).
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerCancel = "pointer-cancel"
// PointerOut is the constant for "pointer-out" property tag.
// The "pointer-out" event is fired for several reasons including: pointing device is moved out
// of the hit test boundaries of an element; firing the pointerup event for a device
// that does not support hover (see "pointer-up"); after firing the pointercancel event (see "pointer-cancel");
// when a pen stylus leaves the hover range detectable by the digitizer.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerOut = "pointer-out"
// PointerOver is the constant for "pointer-over" property tag.
// The "pointer-over" event is fired when a pointing device is moved into an view's hit test boundaries.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerOver = "pointer-over"
)
type PointerEvent struct {
MouseEvent
// PointerID is a unique identifier for the pointer causing the event.
PointerID int
// Width is the width (magnitude on the X axis), in pixels, of the contact geometry of the pointer.
Width float64
// Height is the height (magnitude on the Y axis), in pixels, of the contact geometry of the pointer.
Height float64
// Pressure is the normalized pressure of the pointer input in the range 0 to 1, where 0 and 1 represent
// the minimum and maximum pressure the hardware is capable of detecting, respectively.
Pressure float64
// TangentialPressure is the normalized tangential pressure of the pointer input (also known
// as barrel pressure or cylinder stress) in the range -1 to 1, where 0 is the neutral position of the control.
TangentialPressure float64
// TiltX is the plane angle (in degrees, in the range of -90 to 90) between the YZ plane
// and the plane containing both the pointer (e.g. pen stylus) axis and the Y axis.
TiltX float64
// TiltY is the plane angle (in degrees, in the range of -90 to 90) between the XZ plane
// and the plane containing both the pointer (e.g. pen stylus) axis and the X axis.
TiltY float64
// Twist is the clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees,
// with a value in the range 0 to 359.
Twist float64
// PointerType indicates the device type that caused the event ("mouse", "pen", "touch", etc.)
PointerType string
// IsPrimary indicates if the pointer represents the primary pointer of this pointer type.
IsPrimary bool
}
func valueToPointerListeners(value interface{}) ([]func(View, PointerEvent), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(View, PointerEvent):
return []func(View, PointerEvent){value}, true
case func(PointerEvent):
fn := func(view View, event PointerEvent) {
value(event)
}
return []func(View, PointerEvent){fn}, true
case func(View):
fn := func(view View, event PointerEvent) {
value(view)
}
return []func(View, PointerEvent){fn}, true
case func():
fn := func(view View, event PointerEvent) {
value()
}
return []func(View, PointerEvent){fn}, true
case []func(View, PointerEvent):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(PointerEvent):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, PointerEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event PointerEvent) {
v(event)
}
}
return listeners, true
case []func(View):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, PointerEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event PointerEvent) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, PointerEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event PointerEvent) {
v()
}
}
return listeners, true
case []interface{}:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, PointerEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(View, PointerEvent):
listeners[i] = v
case func(PointerEvent):
listeners[i] = func(view View, event PointerEvent) {
v(event)
}
case func(View):
listeners[i] = func(view View, event PointerEvent) {
v(view)
}
case func():
listeners[i] = func(view View, event PointerEvent) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var pointerEvents = map[string]struct{ jsEvent, jsFunc string }{
PointerDown: {jsEvent: "onpointerdown", jsFunc: "pointerDownEvent"},
PointerUp: {jsEvent: "onpointerup", jsFunc: "pointerUpEvent"},
PointerMove: {jsEvent: "onpointermove", jsFunc: "pointerMoveEvent"},
PointerCancel: {jsEvent: "onpointercancel", jsFunc: "pointerCancelEvent"},
PointerOut: {jsEvent: "onpointerout", jsFunc: "pointerOutEvent"},
PointerOver: {jsEvent: "onpointerover", jsFunc: "pointerOverEvent"},
}
func (view *viewData) setPointerListener(tag string, value interface{}) bool {
listeners, ok := valueToPointerListeners(value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removePointerListener(tag)
} else if js, ok := pointerEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removePointerListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := pointerEvents[tag]; ok {
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
}
}
}
func getPointerListeners(view View, subviewID string, tag string) []func(View, PointerEvent) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(View, PointerEvent)); ok {
return result
}
}
}
return []func(View, PointerEvent){}
}
func pointerEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range pointerEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, PointerEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func (event *PointerEvent) init(data DataObject) {
event.MouseEvent.init(data)
event.PointerID = dataIntProperty(data, "pointerId")
event.Width = dataFloatProperty(data, "width")
event.Height = dataFloatProperty(data, "height")
event.Pressure = dataFloatProperty(data, "pressure")
event.TangentialPressure = dataFloatProperty(data, "tangentialPressure")
event.TiltX = dataFloatProperty(data, "tiltX")
event.TiltY = dataFloatProperty(data, "tiltY")
event.Twist = dataFloatProperty(data, "twist")
value, _ := data.PropertyValue("pointerType")
event.PointerType = value
event.IsPrimary = dataBoolProperty(data, "isPrimary")
}
func handlePointerEvents(view View, tag string, data DataObject) {
listeners := getPointerListeners(view, "", tag)
if len(listeners) == 0 {
return
}
var event PointerEvent
event.init(data)
for _, listener := range listeners {
listener(view, event)
}
}
// GetPointerDownListeners returns the "pointer-down" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetPointerDownListeners(view View, subviewID string) []func(View, PointerEvent) {
return getPointerListeners(view, subviewID, PointerDown)
}
// GetPointerUpListeners returns the "pointer-up" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetPointerUpListeners(view View, subviewID string) []func(View, PointerEvent) {
return getPointerListeners(view, subviewID, PointerUp)
}
// GetPointerMoveListeners returns the "pointer-move" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetPointerMoveListeners(view View, subviewID string) []func(View, PointerEvent) {
return getPointerListeners(view, subviewID, PointerMove)
}
// GetPointerCancelListeners returns the "pointer-cancel" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetPointerCancelListeners(view View, subviewID string) []func(View, PointerEvent) {
return getPointerListeners(view, subviewID, PointerCancel)
}
// GetPointerOverListeners returns the "pointer-over" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetPointerOverListeners(view View, subviewID string) []func(View, PointerEvent) {
return getPointerListeners(view, subviewID, PointerOver)
}
// GetPointerOutListeners returns the "pointer-out" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetPointerOutListeners(view View, subviewID string) []func(View, PointerEvent) {
return getPointerListeners(view, subviewID, PointerOut)
}

310
popup.go Normal file
View File

@ -0,0 +1,310 @@
package rui
import "strings"
const (
// Title is the Popup string property
Title = "title"
// TitleStyle is the Popup string property
TitleStyle = "title-style"
// CloseButton is the Popup bool property
CloseButton = "close-button"
// OutsideClose is the Popup bool property
OutsideClose = "outside-close"
Buttons = "buttons"
ButtonsAlign = "buttons-align"
)
type PopupButton struct {
Title string
OnClick func(Popup)
}
// Popup interface
type Popup interface {
//Properties
View() View
Session() Session
Show()
Dismiss()
html(buffer *strings.Builder)
viewByHTMLID(id string) View
}
type popupData struct {
//propertyList
layerView View
view View
}
type popupManager struct {
popups []Popup
}
func (popup *popupData) init(view View, params Params) {
popup.view = view
props := propertyList{properties: params}
session := view.Session()
var title View = nil
titleStyle := "ruiPopupTitle"
closeButton, _ := boolProperty(&props, CloseButton, session)
outsideClose, _ := boolProperty(&props, OutsideClose, session)
vAlign, _ := enumProperty(&props, VerticalAlign, session, CenterAlign)
hAlign, _ := enumProperty(&props, HorizontalAlign, session, CenterAlign)
buttonsAlign, _ := enumProperty(&props, ButtonsAlign, session, RightAlign)
buttons := []PopupButton{}
if value, ok := params[Buttons]; ok && value != nil {
switch value := value.(type) {
case PopupButton:
buttons = []PopupButton{value}
case []PopupButton:
buttons = value
}
}
popupView := NewGridLayout(view.Session(), Params{
Style: "ruiPopup",
MaxWidth: Percent(100),
MaxHeight: Percent(100),
CellVerticalAlign: StretchAlign,
CellHorizontalAlign: StretchAlign,
ClickEvent: func(View) {},
})
for tag, value := range params {
switch tag {
case Title:
switch value := value.(type) {
case string:
title = NewTextView(view.Session(), Params{Text: value})
case View:
title = value
default:
notCompatibleType(Title, value)
}
case TitleStyle:
switch value := value.(type) {
case string:
titleStyle = value
default:
notCompatibleType(TitleStyle, value)
}
case CloseButton, OutsideClose, VerticalAlign, HorizontalAlign:
// do nothing
default:
popupView.Set(tag, value)
}
}
var cellHeight []SizeUnit
viewRow := 0
if title != nil || closeButton {
viewRow = 1
titleHeight, _ := sizeConstant(popup.Session(), "popupTitleHeight")
titleView := NewGridLayout(session, Params{
Row: 0,
Style: titleStyle,
CellWidth: []SizeUnit{Fr(1), titleHeight},
CellVerticalAlign: CenterAlign,
PaddingLeft: Px(12),
})
if title != nil {
titleView.Append(title)
}
if closeButton {
titleView.Append(NewGridLayout(session, Params{
Column: 1,
Height: titleHeight,
Width: titleHeight,
CellHorizontalAlign: CenterAlign,
CellVerticalAlign: CenterAlign,
TextSize: Px(20),
Content: "✕",
ClickEvent: func(View) {
popup.Dismiss()
},
}))
}
popupView.Append(titleView)
cellHeight = []SizeUnit{AutoSize(), Fr(1)}
} else {
cellHeight = []SizeUnit{Fr(1)}
}
view.Set(Row, viewRow)
popupView.Append(view)
if buttonCount := len(buttons); buttonCount > 0 {
cellHeight = append(cellHeight, AutoSize())
gap, _ := sizeConstant(session, "popupButtonGap")
cellWidth := []SizeUnit{}
for i := 0; i < buttonCount; i++ {
cellWidth = append(cellWidth, Fr(1))
}
buttonsPanel := NewGridLayout(session, Params{
CellWidth: cellWidth,
})
if gap.Type != Auto && gap.Value > 0 {
buttonsPanel.Set(Gap, gap)
buttonsPanel.Set(Margin, gap)
}
createButton := func(n int, button PopupButton) Button {
return NewButton(session, Params{
Column: n,
Content: button.Title,
ClickEvent: func() {
if button.OnClick != nil {
button.OnClick(popup)
} else {
popup.Dismiss()
}
},
})
}
for i, button := range buttons {
buttonsPanel.Append(createButton(i, button))
}
popupView.Append(NewGridLayout(session, Params{
Row: viewRow + 1,
CellHorizontalAlign: buttonsAlign,
Content: buttonsPanel,
}))
}
popupView.Set(CellHeight, cellHeight)
popup.layerView = NewGridLayout(session, Params{
Style: "ruiPopupLayer",
CellVerticalAlign: vAlign,
CellHorizontalAlign: hAlign,
Content: popupView,
MaxWidth: Percent(100),
MaxHeight: Percent(100),
})
if outsideClose {
popup.layerView.Set(ClickEvent, func(View) {
popup.Dismiss()
})
}
}
func (popup popupData) View() View {
return popup.view
}
func (popup *popupData) Session() Session {
return popup.view.Session()
}
func (popup *popupData) Dismiss() {
popup.Session().popupManager().dismissPopup(popup)
// TODO
}
func (popup *popupData) Show() {
popup.Session().popupManager().showPopup(popup)
}
func (popup *popupData) html(buffer *strings.Builder) {
viewHTML(popup.layerView, buffer)
}
func (popup *popupData) viewByHTMLID(id string) View {
return viewByHTMLID(id, popup.layerView)
}
// NewPopup creates a new Popup
func NewPopup(view View, param Params) Popup {
if view == nil {
return nil
}
popup := new(popupData)
popup.init(view, param)
return popup
}
func (manager *popupManager) updatePopupLayerInnerHTML(session Session) {
if manager.popups == nil {
manager.popups = []Popup{}
session.runScript(`updateInnerHTML('ruiPopupLayer', '');`)
return
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`updateInnerHTML('ruiPopupLayer', '`)
for _, p := range manager.popups {
p.html(buffer)
}
buffer.WriteString(`');`)
session.runScript(buffer.String())
}
func (manager *popupManager) showPopup(popup Popup) {
if popup == nil {
return
}
session := popup.Session()
if manager.popups == nil || len(manager.popups) == 0 {
manager.popups = []Popup{popup}
} else {
manager.popups = append(manager.popups, popup)
}
manager.updatePopupLayerInnerHTML(session)
updateCSSProperty("ruiPopupLayer", "visibility", "visible", session)
}
func (manager *popupManager) dismissPopup(popup Popup) {
if manager.popups == nil {
manager.popups = []Popup{}
return
}
count := len(manager.popups)
if count <= 0 || popup == nil {
return
}
session := popup.Session()
if manager.popups[count-1] == popup {
if count == 1 {
manager.popups = []Popup{}
updateCSSProperty("ruiPopupLayer", "visibility", "hidden", session)
session.runScript(`updateInnerHTML('ruiPopupLayer', '');`)
} else {
manager.popups = manager.popups[:count-1]
manager.updatePopupLayerInnerHTML(session)
}
return
}
for n, p := range manager.popups {
if p == popup {
if n == 0 {
manager.popups = manager.popups[1:]
} else {
manager.popups = append(manager.popups[:n], manager.popups[n+1:]...)
}
manager.updatePopupLayerInnerHTML(session)
return
}
}
}

171
popupUtils.go Normal file
View File

@ -0,0 +1,171 @@
package rui
// ShowMessage displays the popup with text message
func ShowMessage(title, text string, session Session) {
textView := NewTextView(session, Params{
Text: text,
Style: "ruiMessageText",
})
params := Params{
CloseButton: true,
OutsideClose: true,
}
if title != "" {
params[Title] = title
}
NewPopup(textView, params).Show()
}
func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) {
textView := NewTextView(session, Params{
Text: text,
Style: "ruiMessageText",
})
params := Params{
CloseButton: false,
OutsideClose: false,
Buttons: []PopupButton{
{
Title: "No",
OnClick: func(popup Popup) {
popup.Dismiss()
if onNo != nil {
onNo()
}
},
},
{
Title: "Yes",
OnClick: func(popup Popup) {
popup.Dismiss()
if onYes != nil {
onYes()
}
},
},
},
}
if title != "" {
params[Title] = title
}
NewPopup(textView, params).Show()
}
func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) {
textView := NewTextView(session, Params{
Text: text,
Style: "ruiMessageText",
})
params := Params{
CloseButton: false,
OutsideClose: false,
Buttons: []PopupButton{
{
Title: "Cancel",
OnClick: func(popup Popup) {
popup.Dismiss()
if onCancel != nil {
onCancel()
}
},
},
{
Title: "No",
OnClick: func(popup Popup) {
popup.Dismiss()
if onNo != nil {
onNo()
}
},
},
{
Title: "Yes",
OnClick: func(popup Popup) {
popup.Dismiss()
if onYes != nil {
onYes()
}
},
},
},
}
if title != "" {
params[Title] = title
}
NewPopup(textView, params).Show()
}
type popupMenuData struct {
items []string
session Session
popup Popup
result func(int)
}
func (popup *popupMenuData) itemClick(list ListView, n int) {
popup.popup.Dismiss()
if popup.result != nil {
popup.result(n)
}
}
func (popup *popupMenuData) ListSize() int {
return len(popup.items)
}
func (popup *popupMenuData) ListItem(index int, session Session) View {
return NewTextView(popup.session, Params{
Text: popup.items[index],
Style: "ruiPopupMenuItem",
})
}
func (popup *popupMenuData) IsListItemEnabled(index int) bool {
return true
}
const PopupMenuResult = "popup-menu-result"
// ShowMenu displays the popup with text message
func ShowMenu(session Session, params Params) bool {
value, ok := params[Items]
if !ok || value == nil {
ErrorLog("Unable to show empty menu")
return false
}
var adapter ListAdapter
data := new(popupMenuData)
data.session = session
switch value := value.(type) {
case []string:
data.items = value
adapter = data
case ListAdapter:
adapter = value
default:
notCompatibleType(Items, value)
return false
}
value, ok = params[PopupMenuResult]
if ok && value != nil {
if result, ok := value.(func(int)); ok {
data.result = result
}
}
listView := NewListView(session, Params{
Items: adapter,
Orientation: TopDownOrientation,
ListItemClickedEvent: data.itemClick,
})
data.popup = NewPopup(listView, params)
data.popup.Show()
FocusView(listView)
return true
}

134
progressBar.go Normal file
View File

@ -0,0 +1,134 @@
package rui
import (
"strconv"
"strings"
)
const (
ProgressBarMax = "progress-max"
ProgressBarValue = "progress-value"
)
// ProgressBar - ProgressBar view
type ProgressBar interface {
View
}
type progressBarData struct {
viewData
}
// NewProgressBar create new ProgressBar object and return it
func NewProgressBar(session Session, params Params) ProgressBar {
view := new(progressBarData)
view.Init(session)
setInitParams(view, params)
return view
}
func newProgressBar(session Session) View {
return NewProgressBar(session, nil)
}
func (progress *progressBarData) Init(session Session) {
progress.viewData.Init(session)
progress.tag = "ProgressBar"
}
func (progress *progressBarData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case Max, "progress-bar-max", "progressbar-max":
return ProgressBarMax
case Value, "progress-bar-value", "progressbar-value":
return ProgressBarValue
}
return tag
}
func (progress *progressBarData) Remove(tag string) {
progress.remove(progress.normalizeTag(tag))
}
func (progress *progressBarData) remove(tag string) {
progress.viewData.remove(tag)
progress.propertyChanged(tag)
}
func (progress *progressBarData) propertyChanged(tag string) {
switch tag {
case ProgressBarMax:
updateProperty(progress.htmlID(), Max, strconv.FormatFloat(GetProgressBarMax(progress, ""), 'f', -1, 32), progress.session)
case ProgressBarValue:
updateProperty(progress.htmlID(), Value, strconv.FormatFloat(GetProgressBarValue(progress, ""), 'f', -1, 32), progress.session)
}
}
func (progress *progressBarData) Set(tag string, value interface{}) bool {
return progress.set(progress.normalizeTag(tag), value)
}
func (progress *progressBarData) set(tag string, value interface{}) bool {
if progress.viewData.set(tag, value) {
progress.propertyChanged(tag)
return true
}
return false
}
func (progress *progressBarData) Get(tag string) interface{} {
return progress.get(progress.normalizeTag(tag))
}
func (progress *progressBarData) htmlTag() string {
return "progress"
}
func (progress *progressBarData) htmlProperties(self View, buffer *strings.Builder) {
progress.viewData.htmlProperties(self, buffer)
buffer.WriteString(` max="`)
buffer.WriteString(strconv.FormatFloat(GetProgressBarMax(progress, ""), 'f', -1, 64))
buffer.WriteByte('"')
buffer.WriteString(` value="`)
buffer.WriteString(strconv.FormatFloat(GetProgressBarValue(progress, ""), 'f', -1, 64))
buffer.WriteByte('"')
}
// GetProgressBarMax returns the max value of ProgressBar subview.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetProgressBarMax(view View, subviewID string) float64 {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return 0
}
result, ok := floatStyledProperty(view, ProgressBarMax, 1)
if !ok {
result, _ = floatStyledProperty(view, Max, 1)
}
return result
}
// GetProgressBarValue returns the value of ProgressBar subview.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetProgressBarValue(view View, subviewID string) float64 {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return 0
}
result, ok := floatStyledProperty(view, ProgressBarValue, 0)
if !ok {
result, _ = floatStyledProperty(view, Value, 0)
}
return result
}

87
properties.go Normal file
View File

@ -0,0 +1,87 @@
package rui
import (
"sort"
"strings"
)
// Properties interface of properties map
type Properties interface {
// Get returns a value of the property with name defined by the argument.
// The type of return value depends on the property. If the property is not set then nil is returned.
Get(tag string) interface{}
getRaw(tag string) interface{}
// Set sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log
Set(tag string, value interface{}) bool
setRaw(tag string, value interface{})
// Remove removes the property with name defined by the argument
Remove(tag string)
// Clear removes all properties
Clear()
// AllTags returns an array of the set properties
AllTags() []string
}
type propertyList struct {
properties map[string]interface{}
}
func (properties *propertyList) init() {
properties.properties = map[string]interface{}{}
}
func (properties *propertyList) Get(tag string) interface{} {
return properties.getRaw(strings.ToLower(tag))
}
func (properties *propertyList) getRaw(tag string) interface{} {
if value, ok := properties.properties[tag]; ok {
return value
}
return nil
}
func (properties *propertyList) setRaw(tag string, value interface{}) {
properties.properties[tag] = value
}
func (properties *propertyList) Remove(tag string) {
delete(properties.properties, strings.ToLower(tag))
}
func (properties *propertyList) remove(tag string) {
delete(properties.properties, tag)
}
func (properties *propertyList) Clear() {
properties.properties = map[string]interface{}{}
}
func (properties *propertyList) AllTags() []string {
tags := make([]string, 0, len(properties.properties))
for t := range properties.properties {
tags = append(tags, t)
}
sort.Strings(tags)
return tags
}
func parseProperties(properties Properties, object DataObject) {
count := object.PropertyCount()
for i := 0; i < count; i++ {
if node := object.Property(i); node != nil {
switch node.Type() {
case TextNode:
properties.Set(node.Tag(), node.Text())
case ObjectNode:
properties.Set(node.Tag(), node.Object())
case ArrayNode:
properties.Set(node.Tag(), node.ArrayElements())
}
}
}
}

141
properties_test.go Normal file
View File

@ -0,0 +1,141 @@
package rui
/*
import (
"testing"
)
func TestProperties(t *testing.T) {
createTestLog(t, true)
list := new(propertyList)
list.init()
if !list.Set("name", "abc") {
t.Error(`list.Set("name", "abc") fail`)
}
if !list.Has("name") {
t.Error(`list.Has("name") fail`)
}
v := list.Get("name")
if v == nil {
t.Error(`list.Get("name") fail`)
}
if text, ok := v.(string); ok {
if text != "abc" {
t.Error(`list.Get("name") != "abc"`)
}
} else {
t.Error(`list.Get("name") is not string`)
}
sizeValues := []interface{}{"@small", "auto", "10px", Pt(20), AutoSize()}
for _, value := range sizeValues {
if !list.setSizeProperty("size", value) {
t.Errorf(`setSizeProperty("size", %v) fail`, value)
}
}
failSizeValues := []interface{}{"@small,big", "abc", "10", Color(20), 100}
for _, value := range failSizeValues {
if list.setSizeProperty("size", value) {
t.Errorf(`setSizeProperty("size", %v) success`, value)
}
}
angleValues := []interface{}{"@angle", "2pi", "π", "3deg", "60°", Rad(1.5), Deg(45), 1, 1.5}
for _, value := range angleValues {
if !list.setAngleProperty("angle", value) {
t.Errorf(`setAngleProperty("angle", %v) fail`, value)
}
}
failAngleValues := []interface{}{"@angle,2", "pi32", "deg", "60°x", Color(0xFFFFFFFF)}
for _, value := range failAngleValues {
if list.setAngleProperty("angle", value) {
t.Errorf(`setAngleProperty("angle", %v) success`, value)
}
}
colorValues := []interface{}{"@color", "#FF234567", "#234567", "rgba(30%, 128, 0.5, .25)", "rgb(30%, 128, 0.5)", Color(0xFFFFFFFF), 0xFFFFFFFF, White}
for _, color := range colorValues {
if !list.setColorProperty("color", color) {
t.Errorf(`list.setColorProperty("color", %v) fail`, color)
}
}
failColorValues := []interface{}{"@color|2", "#FF234567FF", "#TT234567", "rgba(500%, 128, 10.5, .25)", 10.6}
for _, color := range failColorValues {
if list.setColorProperty("color", color) {
t.Errorf(`list.setColorProperty("color", %v) success`, color)
}
}
enumValues := []interface{}{"@enum", "inherit", "on", Inherit, 2}
inheritOffOn := inheritOffOnValues()
for _, value := range enumValues {
if !list.setEnumProperty("enum", value, inheritOffOn) {
t.Errorf(`list.setEnumProperty("enum", %v, %v) fail`, value, inheritOffOn)
}
}
failEnumValues := []interface{}{"@enum 13", "inherit2", "onn", -1, 10}
for _, value := range failEnumValues {
if list.setEnumProperty("enum", value, inheritOffOn) {
t.Errorf(`list.setEnumProperty("enum", %v, %v) success`, value, inheritOffOn)
}
}
boolValues := []interface{}{"@bool", "true", "yes ", "on", " 1", "false", "no", "off", "0", 0, 1, false, true}
for _, value := range boolValues {
if !list.setBoolProperty("bool", value) {
t.Errorf(`list.setBoolProperty("bool", %v) fail`, value)
}
}
failBoolValues := []interface{}{"@bool,2", "tr", "ys", "10", -1, 10, 0.8}
for _, value := range failBoolValues {
if list.setBoolProperty("bool", value) {
t.Errorf(`list.setBoolProperty("bool", %v) success`, value)
}
}
intValues := []interface{}{"@int", " 100", "-10 ", 0, 250}
for _, value := range intValues {
if !list.setIntProperty("int", value) {
t.Errorf(`list.setIntProperty("int", %v) fail`, value)
}
}
failIntValues := []interface{}{"@int|10", "100i", "-1.0 ", 0.0}
for _, value := range failIntValues {
if list.setIntProperty("int", value) {
t.Errorf(`list.setIntProperty("int", %v) success`, value)
}
}
floatValues := []interface{}{"@float", " 100.25", "-1.5e12 ", uint(0), 250, float32(10.2), float64(0)}
for _, value := range floatValues {
if !list.setFloatProperty("float", value) {
t.Errorf(`list.setFloatProperty("float", %v) fail`, value)
}
}
failFloatValues := []interface{}{"@float|2", " 100.25i", "-1.5ee12 ", "abc"}
for _, value := range failFloatValues {
if list.setFloatProperty("float", value) {
t.Errorf(`list.setFloatProperty("float", %v) success`, value)
}
}
boundsValues := []interface{}{"@bounds", "10px,20pt,@bottom,0", Em(2), []interface{}{"@top", Px(10), AutoSize(), "14pt"}}
for _, value := range boundsValues {
if !list.setBoundsProperty("margin", value) {
t.Errorf(`list.setBoundsProperty("margin", %v) fail`, value)
}
}
}
*/

247
propertyGet.go Normal file
View File

@ -0,0 +1,247 @@
package rui
import (
"strconv"
"strings"
)
func stringProperty(properties Properties, tag string, session Session) (string, bool) {
if value := properties.getRaw(tag); value != nil {
if text, ok := value.(string); ok {
return session.resolveConstants(text)
}
}
return "", false
}
func valueToSizeUnit(value interface{}, session Session) (SizeUnit, bool) {
if value != nil {
switch value := value.(type) {
case SizeUnit:
return value, true
case string:
if text, ok := session.resolveConstants(value); ok {
return StringToSizeUnit(text)
}
}
}
return AutoSize(), false
}
func sizeProperty(properties Properties, tag string, session Session) (SizeUnit, bool) {
return valueToSizeUnit(properties.getRaw(tag), session)
}
func angleProperty(properties Properties, tag string, session Session) (AngleUnit, bool) {
if value := properties.getRaw(tag); value != nil {
switch value := value.(type) {
case AngleUnit:
return value, true
case string:
if text, ok := session.resolveConstants(value); ok {
return StringToAngleUnit(text)
}
}
}
return AngleUnit{Type: 0, Value: 0}, false
}
func valueToColor(value interface{}, session Session) (Color, bool) {
if value != nil {
switch value := value.(type) {
case Color:
return value, true
case string:
if len(value) > 1 && value[0] == '@' {
return session.Color(value[1:])
}
return StringToColor(value)
}
}
return Color(0), false
}
func colorProperty(properties Properties, tag string, session Session) (Color, bool) {
return valueToColor(properties.getRaw(tag), session)
}
func valueToEnum(value interface{}, tag string, session Session, defaultValue int) (int, bool) {
if value != nil {
values := enumProperties[tag].values
switch value := value.(type) {
case int:
if value >= 0 && value < len(values) {
return value, true
}
case string:
if text, ok := session.resolveConstants(value); ok {
if tag == Orientation {
switch strings.ToLower(text) {
case "vertical":
value = "up-down"
case "horizontal":
value = "left-to-right"
}
}
if result, ok := enumStringToInt(text, values, true); ok {
return result, true
}
}
}
}
return defaultValue, false
}
func enumStringToInt(value string, enumValues []string, logError bool) (int, bool) {
value = strings.Trim(value, " \t\n\r")
for n, val := range enumValues {
if val == value {
return n, true
}
}
if n, err := strconv.Atoi(value); err == nil {
if n >= 0 && n < len(enumValues) {
return n, true
}
if logError {
ErrorLogF(`Out of bounds: value index = %d, valid values = [%v]`, n, enumValues)
}
return 0, false
}
value = strings.ToLower(value)
for n, val := range enumValues {
if val == value {
return n, true
}
}
if logError {
ErrorLogF(`Unknown "%s" value. Valid values = [%v]`, value, enumValues)
}
return 0, false
}
func enumProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) {
return valueToEnum(properties.getRaw(tag), tag, session, defaultValue)
}
func valueToBool(value interface{}, session Session) (bool, bool) {
if value != nil {
switch value := value.(type) {
case bool:
return value, true
case string:
if text, ok := session.resolveConstants(value); ok {
switch strings.ToLower(text) {
case "true", "yes", "on", "1":
return true, true
case "false", "no", "off", "0":
return false, true
default:
ErrorLog(`The error of converting of "` + text + `" to bool`)
}
}
}
}
return false, false
}
func boolProperty(properties Properties, tag string, session Session) (bool, bool) {
return valueToBool(properties.getRaw(tag), session)
}
func valueToInt(value interface{}, session Session, defaultValue int) (int, bool) {
if value != nil {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
n, err := strconv.Atoi(strings.Trim(text, " \t"))
if err == nil {
return n, true
}
ErrorLog(err.Error())
} else {
n, err := strconv.Atoi(strings.Trim(value, " \t"))
if err == nil {
return n, true
}
ErrorLog(err.Error())
}
default:
return isInt(value)
}
}
return defaultValue, false
}
func intProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) {
return valueToInt(properties.getRaw(tag), session, defaultValue)
}
func valueToFloat(value interface{}, session Session, defaultValue float64) (float64, bool) {
if value != nil {
switch value := value.(type) {
case float64:
return value, true
case string:
if text, ok := session.resolveConstants(value); ok {
f, err := strconv.ParseFloat(text, 64)
if err == nil {
return f, true
}
ErrorLog(err.Error())
}
}
}
return defaultValue, false
}
func floatProperty(properties Properties, tag string, session Session, defaultValue float64) (float64, bool) {
return valueToFloat(properties.getRaw(tag), session, defaultValue)
}
func valueToRange(value interface{}, session Session) (Range, bool) {
if value != nil {
switch value := value.(type) {
case Range:
return value, true
case int:
return Range{First: value, Last: value}, true
case string:
if text, ok := session.resolveConstants(value); ok {
var result Range
if result.setValue(text) {
return result, true
}
}
}
}
return Range{}, false
}
func rangeProperty(properties Properties, tag string, session Session) (Range, bool) {
return valueToRange(properties.getRaw(tag), session)
}

449
propertyNames.go Normal file
View File

@ -0,0 +1,449 @@
package rui
const (
// ID is the constant for the "id" property tag.
ID = "id"
// Style is the constant for the "style" property tag.
Style = "style"
// StyleDisabled is the constant for the "style-disabled" property tag.
StyleDisabled = "style-disabled"
// Disabled is the constant for the "disabled" property tag.
Disabled = "disabled"
// Semantics is the constant for the "semantics" property tag.
Semantics = "semantics"
// Visibility is the constant for the "visibility" property tag.
Visibility = "visibility"
// ZIndex is the constant for the "z-index" property tag.
// The int "z-index" property sets the z-order of a positioned view.
// Overlapping views with a larger z-index cover those with a smaller one.
ZIndex = "z-index"
// Opacity is the constant for the "opacity" property tag.
// The float "opacity" property in [1..0] range sets the opacity of an element.
// Opacity is the degree to which content behind an element is hidden, and is the opposite of transparency.
Opacity = "opacity"
// Row is the constant for the "row" property tag.
Row = "row"
// Column is the constant for the "column" property tag.
Column = "column"
// Left is the constant for the "left" property tag.
// The "left" SizeUnit property participates in specifying the left border position of a positioned view.
// Used only for views placed in an AbsoluteLayout.
Left = "left"
// Right is the constant for the "right" property tag.
// The "right" SizeUnit property participates in specifying the right border position of a positioned view.
// Used only for views placed in an AbsoluteLayout.
Right = "right"
// Top is the constant for the "top" property tag.
// The "top" SizeUnit property participates in specifying the top border position of a positioned view.
// Used only for views placed in an AbsoluteLayout.
Top = "top"
// Bottom is the constant for the "bottom" property tag.
// The "bottom" SizeUnit property participates in specifying the bottom border position of a positioned view.
// Used only for views placed in an AbsoluteLayout.
Bottom = "bottom"
// Width is the constant for the "width" property tag.
// The "width" SizeUnit property sets an view's width.
Width = "width"
// Height is the constant for the "height" property tag.
// The "height" SizeUnit property sets an view's height.
Height = "height"
// MinWidth is the constant for the "min-width" property tag.
// The "width" SizeUnit property sets an view's minimal width.
MinWidth = "min-width"
// MinHeight is the constant for the "min-height" property tag.
// The "height" SizeUnit property sets an view's minimal height.
MinHeight = "min-height"
// MaxWidth is the constant for the "max-width" property tag.
// The "width" SizeUnit property sets an view's maximal width.
MaxWidth = "max-width"
// MaxHeight is the constant for the "max-height" property tag.
// The "height" SizeUnit property sets an view's maximal height.
MaxHeight = "max-height"
// Margin is the constant for the "margin" property tag.
// The "margin" property sets the margin area on all four sides of an element.
// ...
Margin = "margin"
// MarginLeft is the constant for the "margin-left" property tag.
// The "margin-left" SizeUnit property sets the margin area on the left of a view.
// A positive value places it farther from its neighbors, while a negative value places it closer.
MarginLeft = "margin-left"
// MarginRight is the constant for the "margin-right" property tag.
// The "margin-right" SizeUnit property sets the margin area on the right of a view.
// A positive value places it farther from its neighbors, while a negative value places it closer.
MarginRight = "margin-right"
// MarginTop is the constant for the "margin-top" property tag.
// The "margin-top" SizeUnit property sets the margin area on the top of a view.
// A positive value places it farther from its neighbors, while a negative value places it closer.
MarginTop = "margin-top"
// MarginBottom is the constant for the "margin-bottom" property tag.
// The "margin-bottom" SizeUnit property sets the margin area on the bottom of a view.
// A positive value places it farther from its neighbors, while a negative value places it closer.
MarginBottom = "margin-bottom"
// Padding is the constant for the "padding" property tag.
// The "padding" Bounds property sets the padding area on all four sides of a view at once.
// An element's padding area is the space between its content and its border.
Padding = "padding"
// PaddingLeft is the constant for the "padding-left" property tag.
// The "padding-left" SizeUnit property sets the width of the padding area to the left of a view.
PaddingLeft = "padding-left"
// PaddingRight is the constant for the "padding-right" property tag.
// The "padding-right" SizeUnit property sets the width of the padding area to the right of a view.
PaddingRight = "padding-right"
// PaddingTop is the constant for the "padding-top" property tag.
// The "padding-top" SizeUnit property sets the height of the padding area to the top of a view.
PaddingTop = "padding-top"
// PaddingBottom is the constant for the "padding-bottom" property tag.
// The "padding-bottom" SizeUnit property sets the height of the padding area to the bottom of a view.
PaddingBottom = "padding-bottom"
// BackgroundColor is the constant for the "background-color" property tag.
// The "background-color" property sets the background color of a view.
BackgroundColor = "background-color"
// Background is the constant for the "background" property tag.
// The "background" property sets one or more background images and/or gradients on a view.
// ...
Background = "background"
// Cursor is the constant for the "cursor" property tag.
// The "cursor" int property sets the type of mouse cursor, if any, to show when the mouse pointer is over a view
// Valid values are "auto" (0), "default" (1), "none" (2), "context-menu" (3), "help" (4), "pointer" (5),
// "progress" (6), "wait" (7), "cell" (8), "crosshair" (9), "text" (10), "vertical-text" (11), "alias" (12),
// "copy" (13), "move" (14), "no-drop" (15), "not-allowed" (16), "e-resize" (17), "n-resize" (18),
// "ne-resize" (19), "nw-resize" (20), "s-resize" (21), "se-resize" (22), "sw-resize" (23), "w-resize" (24),
// "ew-resize" (25), "ns-resize" (26), "nesw-resize" (27), "nwse-resize" (28), "col-resize" (29),
// "row-resize" (30), "all-scroll" (31), "zoom-in" (32), "zoom-out" (33), "grab" (34), "grabbing" (35).
Cursor = "cursor"
// Border is the constant for the "border" property tag.
// The "border" property sets a view's border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
Border = "border"
// BorderLeft is the constant for the "border-left" property tag.
// The "border-left" property sets a view's left border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
BorderLeft = "border-left"
// BorderRight is the constant for the "border-right" property tag.
// The "border-right" property sets a view's right border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
BorderRight = "border-right"
// BorderTop is the constant for the "border-top" property tag.
// The "border-top" property sets a view's top border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
BorderTop = "border-top"
// BorderBottom is the constant for the "border-bottom" property tag.
// The "border-bottom" property sets a view's bottom border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
BorderBottom = "border-bottom"
// BorderStyle is the constant for the "border-style" property tag.
// The "border-style" property sets the line style for all four sides of a view's border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
BorderStyle = "border-style"
// BorderLeftStyle is the constant for the "border-left-style" property tag.
// The "border-left-style" int property sets the line style of a view's left border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
BorderLeftStyle = "border-left-style"
// BorderRightStyle is the constant for the "border-right-style" property tag.
// The "border-right-style" int property sets the line style of a view's right border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
BorderRightStyle = "border-right-style"
// BorderTopStyle is the constant for the "border-top-style" property tag.
// The "border-top-style" int property sets the line style of a view's top border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
BorderTopStyle = "border-top-style"
// BorderBottomStyle is the constant for the "border-bottom-style" property tag.
// The "border-bottom-style" int property sets the line style of a view's bottom border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
BorderBottomStyle = "border-bottom-style"
// BorderWidth is the constant for the "border-width" property tag.
// The "border-width" property sets the line width for all four sides of a view's border.
BorderWidth = "border-width"
// BorderLeftWidth is the constant for the "border-left-width" property tag.
// The "border-left-width" SizeUnit property sets the line width of a view's left border.
BorderLeftWidth = "border-left-width"
// BorderRightWidth is the constant for the "border-right-width" property tag.
// The "border-right-width" SizeUnit property sets the line width of a view's right border.
BorderRightWidth = "border-right-width"
// BorderTopWidth is the constant for the "border-top-width" property tag.
// The "border-top-width" SizeUnit property sets the line width of a view's top border.
BorderTopWidth = "border-top-width"
// BorderBottomWidth is the constant for the "border-bottom-width" property tag.
// The "border-bottom-width" SizeUnit property sets the line width of a view's bottom border.
BorderBottomWidth = "border-bottom-width"
// BorderColor is the constant for the "border-color" property tag.
// The "border-color" property sets the line color for all four sides of a view's border.
BorderColor = "border-color"
// BorderLeftColor is the constant for the "border-left-color" property tag.
// The "border-left-color" property sets the line color of a view's left border.
BorderLeftColor = "border-left-color"
// BorderRightColor is the constant for the "border-right-color" property tag.
// The "border-right-color" property sets the line color of a view's right border.
BorderRightColor = "border-right-color"
// BorderTopColor is the constant for the "border-top-color" property tag.
// The "border-top-color" property sets the line color of a view's top border.
BorderTopColor = "border-top-color"
// BorderBottomColor is the constant for the "border-bottom-color" property tag.
// The "border-bottom-color" property sets the line color of a view's bottom border.
BorderBottomColor = "border-bottom-color"
// Outline is the constant for the "outline" property tag.
// The "border" property sets a view's outline. It sets the values of an outline width, style, and color.
Outline = "outline"
// OutlineStyle is the constant for the "outline-style" property tag.
// The "outline-style" int property sets the style of an view's outline.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
OutlineStyle = "outline-style"
// OutlineColor is the constant for the "outline-color" property tag.
// The "outline-color" property sets the color of an view's outline.
OutlineColor = "outline-color"
// OutlineWidth is the constant for the "outline-width" property tag.
// The "outline-width" SizeUnit property sets the width of an view's outline.
OutlineWidth = "outline-width"
// Shadow is the constant for the "shadow" property tag.
// The "shadow" property adds shadow effects around a view's frame. A shadow is described
// by X and Y offsets relative to the element, blur and spread radius, and color.
// ...
Shadow = "shadow"
// FontName is the constant for the "font-name" property tag.
// The "font-name" string property specifies a prioritized list of one or more font family names and/or
// generic family names for the selected view. Values are separated by commas to indicate that they are alternatives.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
FontName = "font-name"
// TextColor is the constant for the "text-color" property tag.
// The "color" property sets the foreground color value of a view's text and text decorations.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextColor = "text-color"
// TextSize is the constant for the "text-size" property tag.
// The "text-size" SizeUnit property sets the size of the font.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextSize = "text-size"
// Italic is the constant for the "italic" property tag.
// The "italic" is the bool property. If it is "true" then a text is displayed in italics.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
Italic = "italic"
// SmallCaps is the constant for the "small-caps" property tag.
// The "small-caps" is the bool property. If it is "true" then a text is displayed in small caps.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
SmallCaps = "small-caps"
// Strikethrough is the constant for the "strikethrough" property tag.
// The "strikethrough" is the bool property. If it is "true" then a text is displayed strikethrough.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
Strikethrough = "strikethrough"
// Overline is the constant for the "overline" property tag.
// The "overline" is the bool property. If it is "true" then a text is displayed overlined.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
Overline = "overline"
// Underline is the constant for the "underline" property tag.
// The "underline" is the bool property. If it is "true" then a text is displayed underlined.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
Underline = "underline"
// TextLineThickness is the constant for the "text-decoration-thickness" property tag.
// The "text-decoration-thickness" SizeUnit property sets the stroke thickness of the decoration line that
// is used on text in an element, such as a line-through, underline, or overline.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextLineThickness = "text-line-thickness"
// TextLineStyle is the constant for the "text-decoration-style" property tag.
// The "text-decoration-style" int property sets the style of the lines specified by "text-decoration" property.
// The style applies to all lines that are set with "text-decoration" property.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextLineStyle = "text-line-style"
// TextLineColor is the constant for the "text-decoration-color" property tag.
// The "text-decoration-color" Color property sets the color of the lines specified by "text-decoration" property.
// The color applies to all lines that are set with "text-decoration" property.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextLineColor = "text-line-color"
// TextWeight is the constant for the "text-weight" property tag.
// Valid values are SolidLine (1), DashedLine (2), DottedLine (3), DoubleLine (4) and WavyLine (5).
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextWeight = "text-weight"
// TextAlign is the constant for the "text-align" property tag.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextAlign = "text-align"
// TextIndent is the constant for the "text-indent" property tag.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextIndent = "text-indent"
// TextShadow is the constant for the "text-shadow" property tag.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextShadow = "text-shadow"
// LetterSpacing is the constant for the "letter-spacing" property tag.
// The "letter-spacing" SizeUnit property sets the horizontal spacing behavior between text characters.
// This value is added to the natural spacing between characters while rendering the text.
// Positive values of letter-spacing causes characters to spread farther apart,
// while negative values of letter-spacing bring characters closer together.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
LetterSpacing = "letter-spacing"
// WordSpacing is the constant for the "word-spacing" property tag.
// The "word-spacing" SizeUnit property sets the length of space between words and between tags.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
WordSpacing = "word-spacing"
// LineHeight is the constant for the "line-height" property tag.
// The "line-height" SizeUnit property sets the height of a line box.
// It's commonly used to set the distance between lines of text.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
LineHeight = "line-height"
// WhiteSpace is the constant for the "white-space" property tag.
// The "white-space" int property sets how white space inside an element is handled.
// Valid values are WhiteSpaceNormal (0), WhiteSpaceNowrap (1), WhiteSpacePre (2),
// WhiteSpacePreWrap (3), WhiteSpacePreLine (4), WhiteSpaceBreakSpaces (5)
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
WhiteSpace = "white-space"
// WordBreak is the constant for the "word-break" property tag.
// The "word-break" int property sets whether line breaks appear wherever the text would otherwise overflow its content box.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
WordBreak = "word-break"
// TextTransform is the constant for the "text-transform" property tag.
// The "text-transform" int property specifies how to capitalize an element's text.
// It can be used to make text appear in all-uppercase or all-lowercase, or with each word capitalized.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextTransform = "text-transform"
// TextDirection is the constant for the "text-direction" property tag.
// The "text-direction" int property sets the direction of text, table columns, and horizontal overflow.
// Use 1 (LeftToRightDirection) for languages written from right to left (like Hebrew or Arabic),
// and 2 (RightToLeftDirection) for those written from left to right (like English and most other languages).
// The default value of the property is 0 (SystemTextDirection): use the system text direction.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
TextDirection = "text-direction"
// WritingMode is the constant for the "writing-mode" property tag.
// The "writing-mode" int property sets whether lines of text are laid out horizontally or vertically,
// as well as the direction in which blocks progress
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
WritingMode = "writing-mode"
// VerticalTextOrientation is the constant for the "vertical-text-orientation" property tag.
// The "vertical-text-orientation" int property sets the orientation of the text characters in a line.
// It only affects text in vertical mode ("writing-mode" property).
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
VerticalTextOrientation = "vertical-text-orientation"
// TextTverflow is the constant for the "text-overflow" property tag.
// The "text-overflow" int property sets how hidden overflow content is signaled to users.
// It can be clipped or display an ellipsis ('…'). Valid values are
TextOverflow = "text-overflow"
// Hint is the constant for the "hint" property tag.
// The "hint" string property sets a hint to the user of what can be entered in the control.
Hint = "hint"
// MaxLength is the constant for the "max-length" property tag.
// The "max-length" int property sets the maximum number of characters that the user can enter
MaxLength = "max-length"
// ReadOnly is the constant for the "readonly" property tag.
// This bool property indicates that the user cannot modify the value of the EditView.
ReadOnly = "readonly"
// Content is the constant for the "content" property tag.
Content = "content"
// Items is the constant for the "items" property tag.
Items = "items"
// Current is the constant for the "current" property tag.
Current = "current"
// Type is the constant for the "type" property tag.
Type = "type"
// Pattern is the constant for the "pattern" property tag.
Pattern = "pattern"
// CellWidth is the constant for the "cell-width" property tag.
CellWidth = "cell-width"
// CellHeight is the constant for the "cell-height" property tag.
CellHeight = "cell-height"
// RowGap is the constant for the "row-gap" property tag.
GridRowGap = "grid-row-gap"
// ColumnGap is the constant for the "column-gap" property tag.
GridColumnGap = "grid-column-gap"
// Source is the constant for the "src" property tag.
Source = "src"
// Fit is the constant for the "fit" property tag.
Fit = "fit"
backgroundFit = "background-fit"
// Repeat is the constant for the "repeat" property tag.
Repeat = "repeat"
// Attachment is the constant for the "attachment" property tag.
Attachment = "attachment"
// Clip is the constant for the "clip" property tag.
BackgroundClip = "background-clip"
// Gradient is the constant for the "gradient" property tag.
Gradient = "gradient"
// Direction is the constant for the "direction" property tag.
Direction = "direction"
// Repeating is the constant for the "repeating" property tag.
Repeating = "repeating"
// RadialGradientRadius is the constant for the "radial-gradient-radius" property tag.
RadialGradientRadius = "radial-gradient-radius"
// RadialGradientShape is the constant for the "radial-gradient-shape" property tag.
RadialGradientShape = "radial-gradient-shape"
// Shape is the constant for the "shape" property tag. It's a short form of "radial-gradient-shape"
Shape = "shape"
// CenterX is the constant for the "center-x" property tag.
CenterX = "center-x"
// CenterY is the constant for the "center-x" property tag.
CenterY = "center-y"
// AltText is the constant for the "alt-text" property tag.
AltText = "alt-text"
altProperty = "alt"
// AvoidBreak is the constant for the "avoid-break" property tag.
// The "avoid-break" bool property sets how region breaks should behave inside a generated box.
// If the property value is "true" then fvoids any break from being inserted within the principal box.
// If the property value is "false" then allows, but does not force, any break to be inserted within
// the principal box.
AvoidBreak = "avoid-break"
// ItemWidth is the constant for the "item-width" property tag.
ItemWidth = "item-width"
// ItemHeight is the constant for the "item-height" property tag.
ItemHeight = "item-height"
// Wrap is the constant for the "wrap" property tag.
Wrap = "wrap"
// Min is the constant for the "min" property tag.
Min = "min"
// Max is the constant for the "max" property tag.
Max = "max"
// Step is the constant for the "step" property tag.
Step = "step"
// Value is the constant for the "value" property tag.
Value = "value"
// Orientation is the constant for the "orientation" property tag.
Orientation = "orientation"
// Anchor is the constant for the "anchor" property tag.
Anchor = "anchor"
// Gap is the constant for the "gap" property tag.
Gap = "gap"
// Tabs is the constant for the "tabs" property tag.
Tabs = "tabs"
// TabStyle is the constant for the "tab-style" property tag.
TabStyle = "tab-style"
// CurrentTabStyle is the constant for the "current-tab-style" property tag.
CurrentTabStyle = "current-tab-style"
// Text is the constant for the "text" property tag.
Text = "text"
// VerticalAlign is the constant for the "vertical-align" property tag.
VerticalAlign = "vertical-align"
// HorizontalAlign is the constant for the "horizontal-align" property tag.
// The "horizontal-align" int property sets the horizontal alignment of the content inside a block element
HorizontalAlign = "horizontal-align"
// ImageVerticalAlign is the constant for the "image-vertical-align" property tag.
ImageVerticalAlign = "image-vertical-align"
// ImageHorizontalAlign is the constant for the "image-horizontal-align" property tag.
ImageHorizontalAlign = "image-horizontal-align"
// Checked is the constant for the "checked" property tag.
Checked = "checked"
// ItemVerticalAlign is the constant for the "item-vertical-align" property tag.
ItemVerticalAlign = "item-vertical-align"
// ItemHorizontalAlign is the constant for the "item-horizontal-align" property tag.
ItemHorizontalAlign = "item-horizontal-align"
// ItemCheckbox is the constant for the "checkbox" property tag.
ItemCheckbox = "checkbox"
// CheckboxHorizontalAlign is the constant for the "checkbox-horizontal-align" property tag.
CheckboxHorizontalAlign = "checkbox-horizontal-align"
// CheckboxVerticalAlign is the constant for the "checkbox-vertical-align" property tag.
CheckboxVerticalAlign = "checkbox-vertical-align"
// NotTranslate is the constant for the "not-translate" property tag.
// This bool property indicates that no need to translate the text.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
NotTranslate = "not-translate"
// Filter is the constant for the "filter" property tag.
// The "filter" property applies graphical effects like blur or color shift to a View.
Filter = "filter"
// Clip is the constant for the "clip" property tag.
// The "clip" property creates a clipping region that sets what part of a View should be shown.
Clip = "clip"
// Points is the constant for the "points" property tag.
Points = "points"
// ShapeOutside is the constant for the "shape-outside" property tag.
// The "shape-outside" property defines a shape (which may be non-rectangular) around which adjacent
// inline content should wrap. By default, inline content wraps around its margin box;
// "shape-outside" provides a way to customize this wrapping, making it possible to wrap text around
// complex objects rather than simple boxes.
ShapeOutside = "shape-outside"
// Float is the constant for the "float" property tag.
// The "float" property places a View on the left or right side of its container,
// allowing text and inline Views to wrap around it.
Float = "float"
)

764
propertySet.go Normal file
View File

@ -0,0 +1,764 @@
package rui
import (
"math"
"strconv"
"strings"
)
var colorProperties = []string{
ColorProperty,
BackgroundColor,
TextColor,
BorderColor,
BorderLeftColor,
BorderRightColor,
BorderTopColor,
BorderBottomColor,
OutlineColor,
TextLineColor,
ColorPickerValue,
}
func isPropertyInList(tag string, list []string) bool {
for _, prop := range list {
if prop == tag {
return true
}
}
return false
}
var angleProperties = []string{
Rotate,
SkewX,
SkewY,
}
var boolProperties = []string{
Disabled,
Inset,
BackfaceVisible,
ReadOnly,
Spellcheck,
CloseButton,
OutsideClose,
Italic,
SmallCaps,
Strikethrough,
Overline,
Underline,
Expanded,
AvoidBreak,
NotTranslate,
Controls,
Loop,
Muted,
}
var intProperties = []string{
ZIndex,
HeadHeight,
FootHeight,
RowSpan,
ColumnSpan,
}
var floatProperties = map[string]struct{ min, max float64 }{
ScaleX: {min: -math.MaxFloat64, max: math.MaxFloat64},
ScaleY: {min: -math.MaxFloat64, max: math.MaxFloat64},
ScaleZ: {min: -math.MaxFloat64, max: math.MaxFloat64},
RotateX: {min: 0, max: 1},
RotateY: {min: 0, max: 1},
RotateZ: {min: 0, max: 1},
NumberPickerMax: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerMin: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerStep: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerValue: {min: -math.MaxFloat64, max: math.MaxFloat64},
ProgressBarMax: {min: 0, max: math.MaxFloat64},
ProgressBarValue: {min: 0, max: math.MaxFloat64},
VideoWidth: {min: 0, max: 10000},
VideoHeight: {min: 0, max: 10000},
}
var sizeProperties = map[string]string{
Width: Width,
Height: Height,
MinWidth: MinWidth,
MinHeight: MinHeight,
MaxWidth: MaxWidth,
MaxHeight: MaxHeight,
Left: Left,
Right: Right,
Top: Top,
Bottom: Bottom,
TextSize: "font-size",
TextIndent: TextIndent,
LetterSpacing: LetterSpacing,
WordSpacing: WordSpacing,
LineHeight: LineHeight,
TextLineThickness: "text-decoration-thickness",
GridRowGap: GridRowGap,
GridColumnGap: GridColumnGap,
ColumnWidth: ColumnWidth,
ColumnGap: ColumnGap,
Gap: Gap,
Margin: Margin,
MarginLeft: MarginLeft,
MarginRight: MarginRight,
MarginTop: MarginTop,
MarginBottom: MarginBottom,
Padding: Padding,
PaddingLeft: PaddingLeft,
PaddingRight: PaddingRight,
PaddingTop: PaddingTop,
PaddingBottom: PaddingBottom,
BorderWidth: BorderWidth,
BorderLeftWidth: BorderLeftWidth,
BorderRightWidth: BorderRightWidth,
BorderTopWidth: BorderTopWidth,
BorderBottomWidth: BorderBottomWidth,
OutlineWidth: OutlineWidth,
XOffset: XOffset,
YOffset: YOffset,
BlurRadius: BlurRadius,
SpreadRadius: SpreadRadius,
Perspective: Perspective,
PerspectiveOriginX: PerspectiveOriginX,
PerspectiveOriginY: PerspectiveOriginY,
OriginX: OriginX,
OriginY: OriginY,
OriginZ: OriginZ,
TranslateX: TranslateX,
TranslateY: TranslateY,
TranslateZ: TranslateZ,
Radius: Radius,
RadiusX: RadiusX,
RadiusY: RadiusY,
RadiusTopLeft: RadiusTopLeft,
RadiusTopLeftX: RadiusTopLeftX,
RadiusTopLeftY: RadiusTopLeftY,
RadiusTopRight: RadiusTopRight,
RadiusTopRightX: RadiusTopRightX,
RadiusTopRightY: RadiusTopRightY,
RadiusBottomLeft: RadiusBottomLeft,
RadiusBottomLeftX: RadiusBottomLeftX,
RadiusBottomLeftY: RadiusBottomLeftY,
RadiusBottomRight: RadiusBottomRight,
RadiusBottomRightX: RadiusBottomRightX,
RadiusBottomRightY: RadiusBottomRightY,
ItemWidth: ItemWidth,
ItemHeight: ItemHeight,
CenterX: CenterX,
CenterY: CenterX,
}
var enumProperties = map[string]struct {
values []string
cssTag string
cssValues []string
}{
Semantics: {
[]string{"default", "article", "section", "aside", "header", "main", "footer", "navigation", "figure", "figure-caption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"},
"",
[]string{"div", "article", "section", "aside", "header", "main", "footer", "nav", "figure", "figcaption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"},
},
Visibility: {
[]string{"visible", "invisible", "gone"},
Visibility,
[]string{"visible", "invisible", "gone"},
},
TextAlign: {
[]string{"left", "right", "center", "justify"},
TextAlign,
[]string{"left", "right", "center", "justify"},
},
TextTransform: {
[]string{"none", "capitalize", "lowercase", "uppercase"},
TextTransform,
[]string{"none", "capitalize", "lowercase", "uppercase"},
},
TextWeight: {
[]string{"inherit", "thin", "extra-light", "light", "normal", "medium", "semi-bold", "bold", "extra-bold", "black"},
"font-weight",
[]string{"inherit", "100", "200", "300", "normal", "500", "600", "bold", "800", "900"},
},
WhiteSpace: {
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
WhiteSpace,
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
},
WordBreak: {
[]string{"normal", "break-all", "keep-all", "break-word"},
WordBreak,
[]string{"normal", "break-all", "keep-all", "break-word"},
},
TextOverflow: {
[]string{"clip", "ellipsis"},
TextOverflow,
[]string{"clip", "ellipsis"},
},
WritingMode: {
[]string{"horizontal-top-to-bottom", "horizontal-bottom-to-top", "vertical-right-to-left", "vertical-left-to-right"},
WritingMode,
[]string{"horizontal-tb", "horizontal-bt", "vertical-rl", "vertical-lr"},
},
TextDirection: {
[]string{"system", "left-to-right", "right-to-left"},
"direction",
[]string{"", "ltr", "rtl"},
},
VerticalTextOrientation: {
[]string{"mixed", "upright"},
"text-orientation",
[]string{"mixed", "upright"},
},
TextLineStyle: {
[]string{"inherit", "solid", "dashed", "dotted", "double", "wavy"},
"text-decoration-style",
[]string{"inherit", "solid", "dashed", "dotted", "double", "wavy"},
},
BorderStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
BorderStyle,
[]string{"none", "solid", "dashed", "dotted", "double"},
},
TopStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
"",
[]string{"none", "solid", "dashed", "dotted", "double"},
},
RightStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
"",
[]string{"none", "solid", "dashed", "dotted", "double"},
},
BottomStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
"",
[]string{"none", "solid", "dashed", "dotted", "double"},
},
LeftStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
"",
[]string{"none", "solid", "dashed", "dotted", "double"},
},
OutlineStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
OutlineStyle,
[]string{"none", "solid", "dashed", "dotted", "double"},
},
Tabs: {
[]string{"hidden", "top", "bottom", "left", "right", "left-list", "right-list"},
"",
[]string{"hidden", "top", "bottom", "left", "right", "left-list", "right-list"},
},
NumberPickerType: {
[]string{"editor", "slider"},
"",
[]string{"editor", "slider"},
},
EditViewType: {
[]string{"text", "password", "email", "emails", "url", "phone", "multiline"},
"",
[]string{"text", "password", "email", "emails", "url", "phone", "multiline"},
},
Orientation: {
[]string{"up-down", "start-to-end", "bottom-up", "end-to-start"},
"",
[]string{"column", "row", "column-reverse", "row-reverse"},
},
Wrap: {
[]string{"off", "on", "reverse"},
"",
[]string{"nowrap", "wrap", "wrap-reverse"},
},
"list-orientation": {
[]string{"vertical", "horizontal"},
"",
[]string{"vertical", "horizontal"},
},
VerticalAlign: {
[]string{"top", "bottom", "center", "stretch"},
"",
[]string{"top", "bottom", "center", "stretch"},
},
HorizontalAlign: {
[]string{"left", "right", "center", "stretch"},
"",
[]string{"left", "right", "center", "stretch"},
},
ButtonsAlign: {
[]string{"left", "right", "center", "stretch"},
"",
[]string{"left", "right", "center", "stretch"},
},
CellVerticalAlign: {
[]string{"top", "bottom", "center", "stretch"},
"align-items",
[]string{"start", "end", "center", "stretch"},
},
CellHorizontalAlign: {
[]string{"left", "right", "center", "stretch"},
"justify-items",
[]string{"start", "end", "center", "stretch"},
},
ImageVerticalAlign: {
[]string{"top", "bottom", "center"},
"",
[]string{"top", "bottom", "center"},
},
ImageHorizontalAlign: {
[]string{"left", "right", "center"},
"",
[]string{"left", "right", "center"},
},
ItemVerticalAlign: {
[]string{"top", "bottom", "center", "stretch"},
"",
[]string{"start", "end", "center", "stretch"},
},
ItemHorizontalAlign: {
[]string{"left", "right", "center", "stretch"},
"",
[]string{"start", "end", "center", "stretch"},
},
CheckboxVerticalAlign: {
[]string{"top", "bottom", "center"},
"",
[]string{"start", "end", "center"},
},
CheckboxHorizontalAlign: {
[]string{"left", "right", "center"},
"",
[]string{"start", "end", "center"},
},
TableVerticalAlign: {
[]string{"top", "bottom", "center", "stretch", "baseline"},
"vertical-align",
[]string{"top", "bottom", "middle", "baseline", "baseline"},
},
Anchor: {
[]string{"top", "bottom"},
"",
[]string{"top", "bottom"},
},
Cursor: {
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
Cursor,
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
},
Fit: {
[]string{"none", "contain", "cover", "fill", "scale-down"},
"object-fit",
[]string{"none", "contain", "cover", "fill", "scale-down"},
},
backgroundFit: {
[]string{"none", "contain", "cover"},
"",
[]string{"none", "contain", "cover"},
},
Repeat: {
[]string{"no-repeat", "repeat", "repeat-x", "repeat-y", "round", "space"},
"",
[]string{"no-repeat", "repeat", "repeat-x", "repeat-y", "round", "space"},
},
Attachment: {
[]string{"scroll", "fixed", "local"},
"",
[]string{"scroll", "fixed", "local"},
},
BackgroundClip: {
[]string{"border-box", "padding-box", "content-box"}, // "text"},
"background-clip",
[]string{"border-box", "padding-box", "content-box"}, // "text"},
},
Direction: {
[]string{"to-top", "to-right-top", "to-right", "to-right-bottom", "to-bottom", "to-left-bottom", "to-left", "to-left-top"},
"",
[]string{"to top", "to right top", "to right", "to right bottom", "to bottom", "to left bottom", "to left", "to left top"},
},
RadialGradientShape: {
[]string{"ellipse", "circle"},
"",
[]string{"ellipse", "circle"},
},
RadialGradientRadius: {
[]string{"closest-side", "closest-corner", "farthest-side", "farthest-corner"},
"",
[]string{"closest-side", "closest-corner", "farthest-side", "farthest-corner"},
},
ItemCheckbox: {
[]string{"none", "single", "multiple"},
"",
[]string{"none", "single", "multiple"},
},
Float: {
[]string{"none", "left", "right"},
"float",
[]string{"none", "left", "right"},
},
Preload: {
[]string{"none", "metadata", "auto"},
"",
[]string{"none", "metadata", "auto"},
},
}
func notCompatibleType(tag string, value interface{}) {
ErrorLogF(`"%T" type not compatible with "%s" property`, value, tag)
}
func invalidPropertyValue(tag string, value interface{}) {
ErrorLogF(`Invalid value "%v" of "%s" property`, value, tag)
}
func isConstantName(text string) bool {
len := len(text)
if len <= 1 || text[0] != '@' {
return false
}
if len > 2 {
last := len - 1
if (text[1] == '`' && text[last] == '`') ||
(text[1] == '"' && text[last] == '"') ||
(text[1] == '\'' && text[last] == '\'') {
return true
}
}
return !strings.ContainsAny(text, ",;|\"'`+(){}[]<>/\\*&%! \t\n\r")
}
func isInt(value interface{}) (int, bool) {
var n int
switch value := value.(type) {
case int:
n = value
case int8:
n = int(value)
case int16:
n = int(value)
case int32:
n = int(value)
case int64:
n = int(value)
case uint:
n = int(value)
case uint8:
n = int(value)
case uint16:
n = int(value)
case uint32:
n = int(value)
case uint64:
n = int(value)
default:
return 0, false
}
return n, true
}
func (properties *propertyList) setSimpleProperty(tag string, value interface{}) bool {
if value == nil {
delete(properties.properties, tag)
return true
} else if text, ok := value.(string); ok {
text = strings.Trim(text, " \t\n\r")
if text == "" {
delete(properties.properties, tag)
return true
}
if isConstantName(text) {
properties.properties[tag] = text
return true
}
}
return false
}
func (properties *propertyList) setSizeProperty(tag string, value interface{}) bool {
if !properties.setSimpleProperty(tag, value) {
var size SizeUnit
switch value := value.(type) {
case string:
var ok bool
if size, ok = StringToSizeUnit(value); !ok {
invalidPropertyValue(tag, value)
return false
}
case SizeUnit:
size = value
case float32:
size.Type = SizeInPixel
size.Value = float64(value)
case float64:
size.Type = SizeInPixel
size.Value = value
default:
if n, ok := isInt(value); ok {
size.Type = SizeInPixel
size.Value = float64(n)
} else {
notCompatibleType(tag, value)
return false
}
}
if size.Type == Auto {
delete(properties.properties, tag)
} else {
properties.properties[tag] = size
}
}
return true
}
func (properties *propertyList) setAngleProperty(tag string, value interface{}) bool {
if !properties.setSimpleProperty(tag, value) {
var angle AngleUnit
switch value := value.(type) {
case string:
var ok bool
if angle, ok = StringToAngleUnit(value); !ok {
invalidPropertyValue(tag, value)
return false
}
case AngleUnit:
angle = value
case float32:
angle = Rad(float64(value))
case float64:
angle = Rad(value)
default:
if n, ok := isInt(value); ok {
angle = Rad(float64(n))
} else {
notCompatibleType(tag, value)
return false
}
}
properties.properties[tag] = angle
}
return true
}
func (properties *propertyList) setColorProperty(tag string, value interface{}) bool {
if !properties.setSimpleProperty(tag, value) {
var result Color
switch value := value.(type) {
case string:
var ok bool
if result, ok = StringToColor(value); !ok {
invalidPropertyValue(tag, value)
return false
}
case Color:
result = value
default:
if color, ok := isInt(value); ok {
result = Color(color)
} else {
notCompatibleType(tag, value)
return false
}
}
if result == 0 {
delete(properties.properties, tag)
} else {
properties.properties[tag] = result
}
}
return true
}
func (properties *propertyList) setEnumProperty(tag string, value interface{}, values []string) bool {
if !properties.setSimpleProperty(tag, value) {
var n int
if text, ok := value.(string); ok {
if n, ok = enumStringToInt(text, values, false); !ok {
invalidPropertyValue(tag, value)
return false
}
} else if i, ok := isInt(value); ok {
if i < 0 || i >= len(values) {
invalidPropertyValue(tag, value)
return false
}
n = i
} else {
notCompatibleType(tag, value)
return false
}
properties.properties[tag] = n
}
return true
}
func (properties *propertyList) setBoolProperty(tag string, value interface{}) bool {
if !properties.setSimpleProperty(tag, value) {
if text, ok := value.(string); ok {
switch strings.ToLower(strings.Trim(text, " \t")) {
case "true", "yes", "on", "1":
properties.properties[tag] = true
case "false", "no", "off", "0":
properties.properties[tag] = false
default:
invalidPropertyValue(tag, value)
return false
}
} else if n, ok := isInt(value); ok {
switch n {
case 1:
properties.properties[tag] = true
case 0:
properties.properties[tag] = false
default:
invalidPropertyValue(tag, value)
return false
}
} else if b, ok := value.(bool); ok {
properties.properties[tag] = b
} else {
notCompatibleType(tag, value)
return false
}
}
return true
}
func (properties *propertyList) setIntProperty(tag string, value interface{}) bool {
if !properties.setSimpleProperty(tag, value) {
if text, ok := value.(string); ok {
n, err := strconv.Atoi(strings.Trim(text, " \t"))
if err != nil {
invalidPropertyValue(tag, value)
ErrorLog(err.Error())
return false
}
properties.properties[tag] = n
} else if n, ok := isInt(value); ok {
properties.properties[tag] = n
} else {
notCompatibleType(tag, value)
return false
}
}
return true
}
func (properties *propertyList) setFloatProperty(tag string, value interface{}, min, max float64) bool {
if !properties.setSimpleProperty(tag, value) {
f := float64(0)
switch value := value.(type) {
case string:
var err error
if f, err = strconv.ParseFloat(strings.Trim(value, " \t"), 64); err != nil {
invalidPropertyValue(tag, value)
ErrorLog(err.Error())
return false
}
case float32:
f = float64(value)
case float64:
f = value
default:
if n, ok := isInt(value); ok {
f = float64(n)
} else {
notCompatibleType(tag, value)
return false
}
}
if f >= min && f <= max {
properties.properties[tag] = f
} else {
ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
return false
}
}
return true
}
func (properties *propertyList) Set(tag string, value interface{}) bool {
return properties.set(strings.ToLower(tag), value)
}
func (properties *propertyList) set(tag string, value interface{}) bool {
if value == nil {
delete(properties.properties, tag)
return true
}
if _, ok := sizeProperties[tag]; ok {
return properties.setSizeProperty(tag, value)
}
if valuesData, ok := enumProperties[tag]; ok {
return properties.setEnumProperty(tag, value, valuesData.values)
}
if limits, ok := floatProperties[tag]; ok {
return properties.setFloatProperty(tag, value, limits.min, limits.max)
}
if isPropertyInList(tag, colorProperties) {
return properties.setColorProperty(tag, value)
}
if isPropertyInList(tag, angleProperties) {
return properties.setAngleProperty(tag, value)
}
if isPropertyInList(tag, boolProperties) {
return properties.setBoolProperty(tag, value)
}
if isPropertyInList(tag, intProperties) {
return properties.setIntProperty(tag, value)
}
if text, ok := value.(string); ok {
properties.properties[tag] = text
return true
}
notCompatibleType(tag, value)
return false
}

201
propertyValues.go Normal file
View File

@ -0,0 +1,201 @@
package rui
const (
// Visible - default value of the view Visibility property: View is visible
Visible = 0
// Invisible - value of the view Visibility property: View is invisible but takes place
Invisible = 1
// Gone - value of the view Visibility property: View is invisible and does not take place
Gone = 2
// NoneTextTransform - not transform text
NoneTextTransform = 0
// CapitalizeTextTransform - capitalize text
CapitalizeTextTransform = 1
// LowerCaseTextTransform - transform text to lower case
LowerCaseTextTransform = 2
// UpperCaseTextTransform - transform text to upper case
UpperCaseTextTransform = 3
// HorizontalTopToBottom - content flows horizontally from left to right, vertically from top to bottom.
// The next horizontal line is positioned below the previous line.
HorizontalTopToBottom = 0
// HorizontalBottomToTop - content flows horizontally from left to right, vertically from bottom to top.
// The next horizontal line is positioned above the previous line.
HorizontalBottomToTop = 1
// VerticalRightToLeft - content flows vertically from top to bottom, horizontally from right to left.
// The next vertical line is positioned to the left of the previous line.
VerticalRightToLeft = 2
// VerticalLeftToRight - content flows vertically from top to bottom, horizontally from left to right.
// The next vertical line is positioned to the right of the previous line.
VerticalLeftToRight = 3
// MixedTextOrientation - rotates the characters of horizontal scripts 90° clockwise.
// Lays out the characters of vertical scripts naturally. Default value.
MixedTextOrientation = 0
// UprightTextOrientation - lays out the characters of horizontal scripts naturally (upright),
// as well as the glyphs for vertical scripts. Note that this keyword causes all characters
// to be considered as left-to-right: the used value of "text-direction" is forced to be "left-to-right".
UprightTextOrientation = 1
// SystemTextDirection - direction of a text and other elements defined by system. This is the default value.
SystemTextDirection = 0
// LeftToRightDirection - text and other elements go from left to right.
LeftToRightDirection = 1
//RightToLeftDirection - text and other elements go from right to left.
RightToLeftDirection = 2
// ThinFont - the value of "text-weight" property: the thin (hairline) text weight
ThinFont = 1
// ExtraLightFont - the value of "text-weight" property: the extra light (ultra light) text weight
ExtraLightFont = 2
// LightFont - the value of "text-weight" property: the light text weight
LightFont = 3
// NormalFont - the value of "text-weight" property (default value): the normal text weight
NormalFont = 4
// MediumFont - the value of "text-weight" property: the medium text weight
MediumFont = 5
// SemiBoldFont - the value of "text-weight" property: the semi bold (demi bold) text weight
SemiBoldFont = 6
// BoldFont - the value of "text-weight" property: the bold text weight
BoldFont = 7
// ExtraBoldFont - the value of "text-weight" property: the extra bold (ultra bold) text weight
ExtraBoldFont = 8
// BlackFont - the value of "text-weight" property: the black (heavy) text weight
BlackFont = 9
// TopAlign - top vertical-align for the "vertical-align" property
TopAlign = 0
// BottomAlign - bottom vertical-align for the "vertical-align" property
BottomAlign = 1
// LeftAlign - the left horizontal-align for the "horizontal-align" property
LeftAlign = 0
// RightAlign - the right horizontal-align for the "horizontal-align" property
RightAlign = 1
// CenterAlign - the center horizontal/vertical-align for the "horizontal-align"/"vertical-align" property
CenterAlign = 2
// StretchAlign - the stretch horizontal/vertical-align for the "horizontal-align"/"vertical-align" property
StretchAlign = 3
// JustifyAlign - the justify text align for "text-align" property
JustifyAlign = 3
// BaselineAlign - the baseline cell-vertical-align for the "cell-vertical-align" property
BaselineAlign = 4
// WhiteSpaceNormal - sequences of white space are collapsed. Newline characters in the source
// are handled the same as other white space. Lines are broken as necessary to fill line boxes.
WhiteSpaceNormal = 0
// WhiteSpaceNowrap - collapses white space as for normal, but suppresses line breaks (text wrapping)
// within the source.
WhiteSpaceNowrap = 1
// WhiteSpacePre - sequences of white space are preserved. Lines are only broken at newline
// characters in the source and at <br> elements.
WhiteSpacePre = 2
// WhiteSpacePreWrap - Sequences of white space are preserved. Lines are broken at newline
// characters, at <br>, and as necessary to fill line boxes.
WhiteSpacePreWrap = 3
// WhiteSpacePreLine - sequences of white space are collapsed. Lines are broken at newline characters,
// at <br>, and as necessary to fill line boxes.
WhiteSpacePreLine = 4
// WhiteSpaceBreakSpaces - the behavior is identical to that of WhiteSpacePreWrap, except that:
// * Any sequence of preserved white space always takes up space, including at the end of the line.
// * A line breaking opportunity exists after every preserved white space character,
// including between white space characters.
// * Such preserved spaces take up space and do not hang, and thus affect the boxs intrinsic sizes
// (min-content size and max-content size).
WhiteSpaceBreakSpaces = 5
// WordBreakNormal - use the default line break rule.
WordBreakNormal = 0
// WordBreakAll - to prevent overflow, word breaks should be inserted between any two characters
// (excluding Chinese/Japanese/Korean text).
WordBreakAll = 1
// WordBreakKeepAll - word breaks should not be used for Chinese/Japanese/Korean (CJK) text.
// Non-CJK text behavior is the same as for normal.
WordBreakKeepAll = 2
// WordBreakWord - when the block boundaries are exceeded, the remaining whole words can be split
// in an arbitrary place, unless a more suitable place for the line break is found.
WordBreakWord = 3
// TextOverflowClip - truncate the text at the limit of the content area, therefore the truncation
// can happen in the middle of a character.
TextOverflowClip = 0
// TextOverflowEllipsis - display an ellipsis ('…', U+2026 HORIZONTAL ELLIPSIS) to represent clipped text.
// The ellipsis is displayed inside the content area, decreasing the amount of text displayed.
// If there is not enough space to display the ellipsis, it is clipped.
TextOverflowEllipsis = 1
// DefaultSemantics - default value of the view Semantic property
DefaultSemantics = 0
// ArticleSemantics - value of the view Semantic property: view represents a self-contained
// composition in a document, page, application, or site, which is intended to be
// independently distributable or reusable (e.g., in syndication)
ArticleSemantics = 1
// SectionSemantics - value of the view Semantic property: view represents
// a generic standalone section of a document, which doesn't have a more specific
// semantic element to represent it.
SectionSemantics = 2
// AsideSemantics - value of the view Semantic property: view represents a portion
// of a document whose content is only indirectly related to the document's main content.
// Asides are frequently presented as sidebars or call-out boxes.
AsideSemantics = 3
// HeaderSemantics - value of the view Semantic property: view represents introductory
// content, typically a group of introductory or navigational aids. It may contain
// some heading elements but also a logo, a search form, an author name, and other elements.
HeaderSemantics = 4
// MainSemantics - value of the view Semantic property: view represents the dominant content
// of the application. The main content area consists of content that is directly related
// to or expands upon the central topic of a document, or the central functionality of an application.
MainSemantics = 5
// FooterSemantics - value of the view Semantic property: view represents a footer for its
// nearest sectioning content or sectioning root element. A footer view typically contains
// information about the author of the section, copyright data or links to related documents.
FooterSemantics = 6
// NavigationSemantics - value of the view Semantic property: view represents a section of
// a page whose purpose is to provide navigation links, either within the current document
// or to other documents. Common examples of navigation sections are menus, tables of contents,
// and indexes.
NavigationSemantics = 7
// FigureSemantics - value of the view Semantic property: view represents self-contained content,
// potentially with an optional caption, which is specified using the FigureCaptionSemantics view.
FigureSemantics = 8
// FigureCaptionSemantics - value of the view Semantic property: view represents a caption or
// legend describing the rest of the contents of its parent FigureSemantics view.
FigureCaptionSemantics = 9
// ButtonSemantics - value of the view Semantic property: view a clickable button
ButtonSemantics = 10
// ParagraphSemantics - value of the view Semantic property: view represents a paragraph.
// Paragraphs are usually represented in visual media as blocks of text separated
// from adjacent blocks by blank lines and/or first-line indentation
ParagraphSemantics = 11
// H1Semantics - value of the view Semantic property: view represent of first level section headings.
// H1Semantics is the highest section level and H6Semantics is the lowest.
H1Semantics = 12
// H2Semantics - value of the view Semantic property: view represent of second level section headings.
// H1Semantics is the highest section level and H6Semantics is the lowest.
H2Semantics = 13
// H3Semantics - value of the view Semantic property: view represent of third level section headings.
// H1Semantics is the highest section level and H6Semantics is the lowest.
H3Semantics = 14
// H4Semantics - value of the view Semantic property: view represent of fourth level section headings.
// H1Semantics is the highest section level and H6Semantics is the lowest.
H4Semantics = 15
// H5Semantics - value of the view Semantic property: view represent of fifth level section headings.
// H1Semantics is the highest section level and H6Semantics is the lowest.
H5Semantics = 16
// H6Semantics - value of the view Semantic property: view represent of sixth level section headings.
// H1Semantics is the highest section level and H6Semantics is the lowest.
H6Semantics = 17
// BlockquoteSemantics - value of the view Semantic property: view indicates that
// the enclosed text is an extended quotation.
BlockquoteSemantics = 18
// CodeSemantics - value of the view Semantic property: view displays its contents styled
// in a fashion intended to indicate that the text is a short fragment of computer code
CodeSemantics = 19
// NoneFloat - value of the view "float" property: the View must not float.
NoneFloat = 0
// LeftFloat - value of the view "float" property: the View must float on the left side of its containing block.
LeftFloat = 1
// RightFloat - value of the view "float" property: the View must float on the right side of its containing block.
RightFloat = 2
)

770
radius.go Normal file
View File

@ -0,0 +1,770 @@
package rui
import (
"fmt"
"strings"
)
const (
// Radius is the SizeUnit view property that determines the corners rounding radius
// of an element's outer border edge.
Radius = "radius"
// RadiusX is the SizeUnit view property that determines the x-axis corners elliptic rounding
// radius of an element's outer border edge.
RadiusX = "radius-x"
// RadiusY is the SizeUnit view property that determines the y-axis corners elliptic rounding
// radius of an element's outer border edge.
RadiusY = "radius-y"
// RadiusTopLeft is the SizeUnit view property that determines the top-left corner rounding radius
// of an element's outer border edge.
RadiusTopLeft = "radius-top-left"
// RadiusTopLeftX is the SizeUnit view property that determines the x-axis top-left corner elliptic
// rounding radius of an element's outer border edge.
RadiusTopLeftX = "radius-top-left-x"
// RadiusTopLeftY is the SizeUnit view property that determines the y-axis top-left corner elliptic
// rounding radius of an element's outer border edge.
RadiusTopLeftY = "radius-top-left-y"
// RadiusTopRight is the SizeUnit view property that determines the top-right corner rounding radius
// of an element's outer border edge.
RadiusTopRight = "radius-top-right"
// RadiusTopRightX is the SizeUnit view property that determines the x-axis top-right corner elliptic
// rounding radius of an element's outer border edge.
RadiusTopRightX = "radius-top-right-x"
// RadiusTopRightY is the SizeUnit view property that determines the y-axis top-right corner elliptic
// rounding radius of an element's outer border edge.
RadiusTopRightY = "radius-top-right-y"
// RadiusBottomLeft is the SizeUnit view property that determines the bottom-left corner rounding radius
// of an element's outer border edge.
RadiusBottomLeft = "radius-bottom-left"
// RadiusBottomLeftX is the SizeUnit view property that determines the x-axis bottom-left corner elliptic
// rounding radius of an element's outer border edge.
RadiusBottomLeftX = "radius-bottom-left-x"
// RadiusBottomLeftY is the SizeUnit view property that determines the y-axis bottom-left corner elliptic
// rounding radius of an element's outer border edge.
RadiusBottomLeftY = "radius-bottom-left-y"
// RadiusBottomRight is the SizeUnit view property that determines the bottom-right corner rounding radius
// of an element's outer border edge.
RadiusBottomRight = "radius-bottom-right"
// RadiusBottomRightX is the SizeUnit view property that determines the x-axis bottom-right corner elliptic
// rounding radius of an element's outer border edge.
RadiusBottomRightX = "radius-bottom-right-x"
// RadiusBottomRightY is the SizeUnit view property that determines the y-axis bottom-right corner elliptic
// rounding radius of an element's outer border edge.
RadiusBottomRightY = "radius-bottom-right-y"
// X is the SizeUnit property of the ShadowProperty that determines the x-axis corners elliptic rounding
// radius of an element's outer border edge.
X = "x"
// Y is the SizeUnit property of the ShadowProperty that determines the y-axis corners elliptic rounding
// radius of an element's outer border edge.
Y = "y"
// TopLeft is the SizeUnit property of the ShadowProperty that determines the top-left corner rounding radius
// of an element's outer border edge.
TopLeft = "top-left"
// TopLeftX is the SizeUnit property of the ShadowProperty that determines the x-axis top-left corner elliptic
// rounding radius of an element's outer border edge.
TopLeftX = "top-left-x"
// TopLeftY is the SizeUnit property of the ShadowProperty that determines the y-axis top-left corner elliptic
// rounding radius of an element's outer border edge.
TopLeftY = "top-left-y"
// TopRight is the SizeUnit property of the ShadowProperty that determines the top-right corner rounding radius
// of an element's outer border edge.
TopRight = "top-right"
// TopRightX is the SizeUnit property of the ShadowProperty that determines the x-axis top-right corner elliptic
// rounding radius of an element's outer border edge.
TopRightX = "top-right-x"
// TopRightY is the SizeUnit property of the ShadowProperty that determines the y-axis top-right corner elliptic
// rounding radius of an element's outer border edge.
TopRightY = "top-right-y"
// BottomLeft is the SizeUnit property of the ShadowProperty that determines the bottom-left corner rounding radius
// of an element's outer border edge.
BottomLeft = "bottom-left"
// BottomLeftX is the SizeUnit property of the ShadowProperty that determines the x-axis bottom-left corner elliptic
// rounding radius of an element's outer border edge.
BottomLeftX = "bottom-left-x"
// BottomLeftY is the SizeUnit property of the ShadowProperty that determines the y-axis bottom-left corner elliptic
// rounding radius of an element's outer border edge.
BottomLeftY = "bottom-left-y"
// BottomRight is the SizeUnit property of the ShadowProperty that determines the bottom-right corner rounding radius
// of an element's outer border edge.
BottomRight = "bottom-right"
// BottomRightX is the SizeUnit property of the ShadowProperty that determines the x-axis bottom-right corner elliptic
// rounding radius of an element's outer border edge.
BottomRightX = "bottom-right-x"
// BottomRightY is the SizeUnit property of the ShadowProperty that determines the y-axis bottom-right corner elliptic
// rounding radius of an element's outer border edge.
BottomRightY = "bottom-right-y"
)
type RadiusProperty interface {
Properties
ruiStringer
fmt.Stringer
BoxRadius(session Session) BoxRadius
}
type radiusPropertyData struct {
propertyList
}
// NewRadiusProperty creates the new RadiusProperty
func NewRadiusProperty(params Params) RadiusProperty {
result := new(radiusPropertyData)
result.properties = map[string]interface{}{}
if params != nil {
for _, tag := range []string{X, Y, TopLeft, TopRight, BottomLeft, BottomRight, TopLeftX, TopLeftY,
TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY} {
if value, ok := params[tag]; ok {
result.Set(tag, value)
}
}
}
return result
}
func (radius *radiusPropertyData) normalizeTag(tag string) string {
return strings.TrimPrefix(strings.ToLower(tag), "radius-")
}
func (radius *radiusPropertyData) ruiString(writer ruiWriter) {
writer.startObject("_")
for _, tag := range []string{X, Y, TopLeft, TopLeftX, TopLeftY, TopRight, TopRightX, TopRightY,
BottomLeft, BottomLeftX, BottomLeftY, BottomRight, BottomRightX, BottomRightY} {
if value, ok := radius.properties[tag]; ok {
writer.writeProperty(Style, value)
}
}
writer.endObject()
}
func (radius *radiusPropertyData) String() string {
writer := newRUIWriter()
radius.ruiString(writer)
return writer.finish()
}
func (radius *radiusPropertyData) delete(tags []string) {
for _, tag := range tags {
delete(radius.properties, tag)
}
}
func (radius *radiusPropertyData) deleteUnusedTags() {
for _, tag := range []string{X, Y} {
if _, ok := radius.properties[tag]; ok {
unused := true
for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} {
if _, ok := radius.properties[t+"-"+tag]; !ok {
if _, ok := radius.properties[t]; !ok {
unused = false
break
}
}
}
if unused {
delete(radius.properties, tag)
}
}
}
equalValue := func(value1, value2 interface{}) bool {
switch value1 := value1.(type) {
case string:
switch value2 := value2.(type) {
case string:
return value1 == value2
}
case SizeUnit:
switch value2 := value2.(type) {
case SizeUnit:
return value1.Equal(value2)
}
}
return false
}
for _, tag := range []string{TopLeft, TopRight, BottomLeft, BottomRight} {
tagX := tag + "-x"
tagY := tag + "-y"
valueX, okX := radius.properties[tagX]
valueY, okY := radius.properties[tagY]
if value, ok := radius.properties[tag]; ok {
if okX && okY {
delete(radius.properties, tag)
} else if okX && !okY {
if equalValue(value, valueX) {
delete(radius.properties, tagX)
} else {
radius.properties[tagY] = value
delete(radius.properties, tag)
}
} else if !okX && okY {
if equalValue(value, valueY) {
delete(radius.properties, tagY)
} else {
radius.properties[tagX] = value
delete(radius.properties, tag)
}
}
} else if okX && okY && equalValue(valueX, valueY) {
radius.properties[tag] = valueX
delete(radius.properties, tagX)
delete(radius.properties, tagY)
}
}
}
func (radius *radiusPropertyData) Remove(tag string) {
tag = radius.normalizeTag(tag)
switch tag {
case X, Y:
if _, ok := radius.properties[tag]; ok {
radius.Set(tag, AutoSize())
delete(radius.properties, tag)
}
case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY:
delete(radius.properties, tag)
case TopLeft, TopRight, BottomLeft, BottomRight:
radius.delete([]string{tag, tag + "-x", tag + "-y"})
default:
ErrorLogF(`"%s" property is not compatible with the RadiusProperty`, tag)
}
}
func (radius *radiusPropertyData) Set(tag string, value interface{}) bool {
if value == nil {
radius.Remove(tag)
return true
}
tag = radius.normalizeTag(tag)
switch tag {
case X:
if radius.setSizeProperty(tag, value) {
radius.delete([]string{TopLeftX, TopRightX, BottomLeftX, BottomRightX})
for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} {
if val, ok := radius.properties[t]; ok {
if _, ok := radius.properties[t+"-y"]; !ok {
radius.properties[t+"-y"] = val
}
delete(radius.properties, t)
}
}
return true
}
case Y:
if radius.setSizeProperty(tag, value) {
radius.delete([]string{TopLeftY, TopRightY, BottomLeftY, BottomRightY})
for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} {
if val, ok := radius.properties[t]; ok {
if _, ok := radius.properties[t+"-x"]; !ok {
radius.properties[t+"-x"] = val
}
delete(radius.properties, t)
}
}
return true
}
case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY:
if radius.setSizeProperty(tag, value) {
radius.deleteUnusedTags()
return true
}
case TopLeft, TopRight, BottomLeft, BottomRight:
switch value := value.(type) {
case SizeUnit:
radius.properties[tag] = value
radius.delete([]string{tag + "-x", tag + "-y"})
radius.deleteUnusedTags()
return true
case string:
if strings.Contains(value, "/") {
if values := strings.Split(value, "/"); len(values) == 2 {
xOK := radius.Set(tag+"-x", value[0])
yOK := radius.Set(tag+"-y", value[1])
return xOK && yOK
} else {
notCompatibleType(tag, value)
}
} else {
if radius.setSizeProperty(tag, value) {
radius.delete([]string{tag + "-x", tag + "-y"})
radius.deleteUnusedTags()
return true
}
}
}
default:
ErrorLogF(`"%s" property is not compatible with the RadiusProperty`, tag)
}
return false
}
func (radius *radiusPropertyData) Get(tag string) interface{} {
tag = radius.normalizeTag(tag)
if value, ok := radius.properties[tag]; ok {
return value
}
switch tag {
case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY:
tagLen := len(tag)
if value, ok := radius.properties[tag[:tagLen-2]]; ok {
return value
}
if value, ok := radius.properties[tag[tagLen-1:]]; ok {
return value
}
}
return nil
}
func (radius *radiusPropertyData) BoxRadius(session Session) BoxRadius {
x, _ := sizeProperty(radius, X, session)
y, _ := sizeProperty(radius, Y, session)
getRadius := func(tag string) (SizeUnit, SizeUnit) {
rx := x
ry := y
if r, ok := sizeProperty(radius, tag, session); ok {
rx = r
ry = r
}
if r, ok := sizeProperty(radius, tag+"-x", session); ok {
rx = r
}
if r, ok := sizeProperty(radius, tag+"-y", session); ok {
ry = r
}
return rx, ry
}
var result BoxRadius
result.TopLeftX, result.TopLeftY = getRadius(TopLeft)
result.TopRightX, result.TopRightY = getRadius(TopRight)
result.BottomLeftX, result.BottomLeftY = getRadius(BottomLeft)
result.BottomRightX, result.BottomRightY = getRadius(BottomRight)
return result
}
// BoxRadius defines radii of rounds the corners of an element's outer border edge
type BoxRadius struct {
TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY SizeUnit
}
// AllAnglesIsEqual returns 'true' if all angles is equal, 'false' otherwise
func (radius BoxRadius) AllAnglesIsEqual() bool {
return radius.TopLeftX.Equal(radius.TopRightX) &&
radius.TopLeftY.Equal(radius.TopRightY) &&
radius.TopLeftX.Equal(radius.BottomLeftX) &&
radius.TopLeftY.Equal(radius.BottomLeftY) &&
radius.TopLeftX.Equal(radius.BottomRightX) &&
radius.TopLeftY.Equal(radius.BottomRightY)
}
// String returns a string representation of a BoxRadius struct
func (radius BoxRadius) String() string {
if radius.AllAnglesIsEqual() {
if radius.TopLeftX.Equal(radius.TopLeftY) {
return radius.TopLeftX.String()
} else {
return fmt.Sprintf("_{ x = %s, y = %s }", radius.TopLeftX.String(), radius.TopLeftY.String())
}
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString("_{ ")
if radius.TopLeftX.Equal(radius.TopLeftY) {
buffer.WriteString("top-left = ")
buffer.WriteString(radius.TopLeftX.String())
} else {
buffer.WriteString("top-left-x = ")
buffer.WriteString(radius.TopLeftX.String())
buffer.WriteString("top-left-y = ")
buffer.WriteString(radius.TopLeftY.String())
}
if radius.TopRightX.Equal(radius.TopRightY) {
buffer.WriteString(", top-right = ")
buffer.WriteString(radius.TopRightX.String())
} else {
buffer.WriteString(", top-right-x = ")
buffer.WriteString(radius.TopRightX.String())
buffer.WriteString(", top-right-y = ")
buffer.WriteString(radius.TopRightY.String())
}
if radius.BottomLeftX.Equal(radius.BottomLeftY) {
buffer.WriteString(", bottom-left = ")
buffer.WriteString(radius.BottomLeftX.String())
} else {
buffer.WriteString(", bottom-left-x = ")
buffer.WriteString(radius.BottomLeftX.String())
buffer.WriteString(", bottom-left-y = ")
buffer.WriteString(radius.BottomLeftY.String())
}
if radius.BottomRightX.Equal(radius.BottomRightY) {
buffer.WriteString(", bottom-right = ")
buffer.WriteString(radius.BottomRightX.String())
} else {
buffer.WriteString(", bottom-right-x = ")
buffer.WriteString(radius.BottomRightX.String())
buffer.WriteString(", bottom-right-y = ")
buffer.WriteString(radius.BottomRightY.String())
}
buffer.WriteString(" }")
return buffer.String()
}
func (radius BoxRadius) cssValue(builder cssBuilder) {
if (radius.TopLeftX.Type == Auto || radius.TopLeftX.Value == 0) &&
(radius.TopLeftY.Type == Auto || radius.TopLeftY.Value == 0) &&
(radius.TopRightX.Type == Auto || radius.TopRightX.Value == 0) &&
(radius.TopRightY.Type == Auto || radius.TopRightY.Value == 0) &&
(radius.BottomRightX.Type == Auto || radius.BottomRightX.Value == 0) &&
(radius.BottomRightY.Type == Auto || radius.BottomRightY.Value == 0) &&
(radius.BottomLeftX.Type == Auto || radius.BottomLeftX.Value == 0) &&
(radius.BottomLeftY.Type == Auto || radius.BottomLeftY.Value == 0) {
return
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(radius.TopLeftX.cssString("0"))
if radius.AllAnglesIsEqual() {
if !radius.TopLeftX.Equal(radius.TopLeftY) {
buffer.WriteString(" / ")
buffer.WriteString(radius.TopLeftY.cssString("0"))
}
} else {
buffer.WriteRune(' ')
buffer.WriteString(radius.TopRightX.cssString("0"))
buffer.WriteRune(' ')
buffer.WriteString(radius.BottomRightX.cssString("0"))
buffer.WriteRune(' ')
buffer.WriteString(radius.BottomLeftX.cssString("0"))
if !radius.TopLeftX.Equal(radius.TopLeftY) ||
!radius.TopRightX.Equal(radius.TopRightY) ||
!radius.BottomLeftX.Equal(radius.BottomLeftY) ||
!radius.BottomRightX.Equal(radius.BottomRightY) {
buffer.WriteString(" / ")
buffer.WriteString(radius.TopLeftY.cssString("0"))
buffer.WriteRune(' ')
buffer.WriteString(radius.TopRightY.cssString("0"))
buffer.WriteRune(' ')
buffer.WriteString(radius.BottomRightY.cssString("0"))
buffer.WriteRune(' ')
buffer.WriteString(radius.BottomLeftY.cssString("0"))
}
}
builder.add("border-radius", buffer.String())
}
func (radius BoxRadius) cssString() string {
var builder cssValueBuilder
radius.cssValue(&builder)
return builder.finish()
}
func getRadiusProperty(style Properties) RadiusProperty {
if value := style.Get(Radius); value != nil {
switch value := value.(type) {
case RadiusProperty:
return value
case BoxRadius:
result := NewRadiusProperty(nil)
if value.AllAnglesIsEqual() {
result.Set(X, value.TopLeftX)
result.Set(Y, value.TopLeftY)
} else {
if value.TopLeftX.Equal(value.TopLeftY) {
result.Set(TopLeft, value.TopLeftX)
} else {
result.Set(TopLeftX, value.TopLeftX)
result.Set(TopLeftY, value.TopLeftY)
}
if value.TopRightX.Equal(value.TopRightY) {
result.Set(TopRight, value.TopRightX)
} else {
result.Set(TopRightX, value.TopRightX)
result.Set(TopRightY, value.TopRightY)
}
if value.BottomLeftX.Equal(value.BottomLeftY) {
result.Set(BottomLeft, value.BottomLeftX)
} else {
result.Set(BottomLeftX, value.BottomLeftX)
result.Set(BottomLeftY, value.BottomLeftY)
}
if value.BottomRightX.Equal(value.BottomRightY) {
result.Set(BottomRight, value.BottomRightX)
} else {
result.Set(BottomRightX, value.BottomRightX)
result.Set(BottomRightY, value.BottomRightY)
}
}
return result
case SizeUnit:
return NewRadiusProperty(Params{
X: value,
Y: value,
})
case string:
return NewRadiusProperty(Params{
X: value,
Y: value,
})
}
}
return NewRadiusProperty(nil)
}
func (properties *propertyList) setRadius(value interface{}) bool {
if value == nil {
delete(properties.properties, Radius)
return true
}
switch value := value.(type) {
case RadiusProperty:
properties.properties[Radius] = value
return true
case SizeUnit:
properties.properties[Radius] = value
return true
case BoxRadius:
radius := NewRadiusProperty(nil)
if value.AllAnglesIsEqual() {
radius.Set(X, value.TopLeftX)
radius.Set(Y, value.TopLeftY)
} else {
if value.TopLeftX.Equal(value.TopLeftY) {
radius.Set(TopLeft, value.TopLeftX)
} else {
radius.Set(TopLeftX, value.TopLeftX)
radius.Set(TopLeftY, value.TopLeftY)
}
if value.TopRightX.Equal(value.TopRightY) {
radius.Set(TopRight, value.TopRightX)
} else {
radius.Set(TopRightX, value.TopRightX)
radius.Set(TopRightY, value.TopRightY)
}
if value.BottomLeftX.Equal(value.BottomLeftY) {
radius.Set(BottomLeft, value.BottomLeftX)
} else {
radius.Set(BottomLeftX, value.BottomLeftX)
radius.Set(BottomLeftY, value.BottomLeftY)
}
if value.BottomRightX.Equal(value.BottomRightY) {
radius.Set(BottomRight, value.BottomRightX)
} else {
radius.Set(BottomRightX, value.BottomRightX)
radius.Set(BottomRightY, value.BottomRightY)
}
}
properties.properties[Radius] = radius
return true
case string:
if strings.Contains(value, "/") {
values := strings.Split(value, "/")
if len(values) == 2 {
okX := properties.setRadiusElement(RadiusX, values[0])
okY := properties.setRadiusElement(RadiusY, values[1])
return okX && okY
} else {
notCompatibleType(Radius, value)
}
} else {
return properties.setSizeProperty(Radius, value)
}
case DataObject:
radius := NewRadiusProperty(nil)
for _, tag := range []string{X, Y, TopLeft, TopRight, BottomLeft, BottomRight, TopLeftX, TopLeftY,
TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY} {
if value, ok := value.PropertyValue(tag); ok {
radius.Set(tag, value)
}
}
properties.properties[Radius] = radius
return true
default:
notCompatibleType(Radius, value)
}
return false
}
func (properties *propertyList) removeRadiusElement(tag string) {
if value, ok := properties.properties[Radius]; ok && value != nil {
radius := getRadiusProperty(properties)
radius.Remove(tag)
if len(radius.AllTags()) == 0 {
delete(properties.properties, Radius)
} else {
properties.properties[Radius] = radius
}
}
}
func (properties *propertyList) setRadiusElement(tag string, value interface{}) bool {
if value == nil {
properties.removeRadiusElement(tag)
return true
}
radius := getRadiusProperty(properties)
if radius.Set(tag, value) {
properties.properties[Radius] = radius
return true
}
return false
}
func getRadiusElement(style Properties, tag string) interface{} {
value := style.Get(Radius)
if value != nil {
switch value := value.(type) {
case string:
return value
case SizeUnit:
return value
case RadiusProperty:
return value.Get(tag)
case BoxRadius:
switch tag {
case RadiusX:
if value.TopLeftX.Equal(value.TopRightX) &&
value.TopLeftX.Equal(value.BottomLeftX) &&
value.TopLeftX.Equal(value.BottomRightX) {
return value.TopLeftX
}
case RadiusY:
if value.TopLeftY.Equal(value.TopRightY) &&
value.TopLeftY.Equal(value.BottomLeftY) &&
value.TopLeftY.Equal(value.BottomRightY) {
return value.TopLeftY
}
case RadiusTopLeft:
if value.TopLeftX.Equal(value.TopLeftY) {
return value.TopLeftY
}
case RadiusTopRight:
if value.TopRightX.Equal(value.TopRightY) {
return value.TopRightY
}
case RadiusBottomLeft:
if value.BottomLeftX.Equal(value.BottomLeftY) {
return value.BottomLeftY
}
case RadiusBottomRight:
if value.BottomRightX.Equal(value.BottomRightY) {
return value.BottomRightY
}
case RadiusTopLeftX:
return value.TopLeftX
case RadiusTopLeftY:
return value.TopLeftY
case RadiusTopRightX:
return value.TopRightX
case RadiusTopRightY:
return value.TopRightY
case RadiusBottomLeftX:
return value.BottomLeftX
case RadiusBottomLeftY:
return value.BottomLeftY
case RadiusBottomRightX:
return value.BottomRightX
case RadiusBottomRightY:
return value.BottomRightY
}
}
}
return nil
}
func getRadius(properties Properties, session Session) BoxRadius {
if value := properties.Get(Radius); value != nil {
switch value := value.(type) {
case BoxRadius:
return value
case RadiusProperty:
return value.BoxRadius(session)
case SizeUnit:
return BoxRadius{TopLeftX: value, TopLeftY: value, TopRightX: value, TopRightY: value,
BottomLeftX: value, BottomLeftY: value, BottomRightX: value, BottomRightY: value}
case string:
if text, ok := session.resolveConstants(value); ok {
if size, ok := StringToSizeUnit(text); ok {
return BoxRadius{TopLeftX: size, TopLeftY: size, TopRightX: size, TopRightY: size,
BottomLeftX: size, BottomLeftY: size, BottomRightX: size, BottomRightY: size}
}
}
}
}
return BoxRadius{}
}

449
resizable.go Normal file
View File

@ -0,0 +1,449 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
const (
// Side is the constant for the "side" property tag.
// The "side" int property determines which side of the container is used to resize.
// The value of property is or-combination of TopSide (1), RightSide (2), BottomSide (4), and LeftSide (8)
Side = "side"
// ResizeBorderWidth is the constant for the "resize-border-width" property tag.
// The "ResizeBorderWidth" SizeUnit property determines the width of the resizing border
ResizeBorderWidth = "resize-border-width"
// CellVerticalAlign is the constant for the "cell-vertical-align" property tag.
CellVerticalAlign = "cell-vertical-align"
// CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag.
CellHorizontalAlign = "cell-horizontal-align"
// TopSide is value of the "side" property: the top side is used to resize
TopSide = 1
// RightSide is value of the "side" property: the right side is used to resize
RightSide = 2
// BottomSide is value of the "side" property: the bottom side is used to resize
BottomSide = 4
// LeftSide is value of the "side" property: the left side is used to resize
LeftSide = 8
// AllSides is value of the "side" property: all sides is used to resize
AllSides = TopSide | RightSide | BottomSide | LeftSide
)
// Resizable - grid-container of View
type Resizable interface {
View
ParanetView
}
type resizableData struct {
viewData
content []View
}
// NewResizable create new Resizable object and return it
func NewResizable(session Session, params Params) Resizable {
view := new(resizableData)
view.Init(session)
setInitParams(view, params)
return view
}
func newResizable(session Session) View {
return NewResizable(session, nil)
}
func (resizable *resizableData) Init(session Session) {
resizable.viewData.Init(session)
resizable.tag = "Resizable"
resizable.systemClass = "ruiGridLayout"
resizable.content = []View{}
}
func (resizable *resizableData) Views() []View {
return resizable.content
}
func (resizable *resizableData) Remove(tag string) {
resizable.remove(strings.ToLower(tag))
}
func (resizable *resizableData) remove(tag string) {
switch tag {
case Side:
oldSide := resizable.getSide()
delete(resizable.properties, Side)
if oldSide != resizable.getSide() {
updateInnerHTML(resizable.htmlID(), resizable.Session())
resizable.updateResizeBorderWidth()
}
case ResizeBorderWidth:
w := resizable.resizeBorderWidth()
delete(resizable.properties, ResizeBorderWidth)
if !w.Equal(resizable.resizeBorderWidth()) {
resizable.updateResizeBorderWidth()
}
case Content:
if len(resizable.content) > 0 {
resizable.content = []View{}
updateInnerHTML(resizable.htmlID(), resizable.Session())
}
default:
resizable.viewData.remove(tag)
}
}
func (resizable *resizableData) Set(tag string, value interface{}) bool {
return resizable.set(strings.ToLower(tag), value)
}
func (resizable *resizableData) set(tag string, value interface{}) bool {
if value == nil {
resizable.remove(tag)
return true
}
switch tag {
case Side:
oldSide := resizable.getSide()
ok := resizable.setSide(value)
if ok && oldSide != resizable.getSide() {
updateInnerHTML(resizable.htmlID(), resizable.Session())
resizable.updateResizeBorderWidth()
} else {
notCompatibleType(tag, value)
}
return ok
case ResizeBorderWidth:
w := resizable.resizeBorderWidth()
ok := resizable.setSizeProperty(tag, value)
if ok && !w.Equal(resizable.resizeBorderWidth()) {
resizable.updateResizeBorderWidth()
}
return ok
case Content:
var newContent View = nil
switch value := value.(type) {
case string:
newContent = NewTextView(resizable.Session(), Params{Text: value})
case View:
newContent = value
case DataObject:
if view := CreateViewFromObject(resizable.Session(), value); view != nil {
newContent = view
}
}
if newContent != nil {
if len(resizable.content) == 0 {
resizable.content = []View{newContent}
} else {
resizable.content[0] = newContent
}
updateInnerHTML(resizable.htmlID(), resizable.Session())
return true
}
case CellWidth, CellHeight, GridRowGap, GridColumnGap, CellVerticalAlign, CellHorizontalAlign:
ErrorLogF(`Not supported "%s" property`, tag)
return false
}
return resizable.viewData.set(tag, value)
}
func (resizable *resizableData) Get(tag string) interface{} {
return resizable.get(strings.ToLower(tag))
}
func (resizable *resizableData) getSide() int {
if value := resizable.getRaw(Side); value != nil {
switch value := value.(type) {
case string:
if value, ok := resizable.session.resolveConstants(value); ok {
validValues := map[string]int{
"top": TopSide,
"right": RightSide,
"bottom": BottomSide,
"left": LeftSide,
"all": AllSides,
}
if strings.Contains(value, "|") {
values := strings.Split(value, "|")
sides := 0
for _, val := range values {
if n, err := strconv.Atoi(val); err == nil {
if n < 1 || n > AllSides {
return AllSides
}
sides |= n
} else if n, ok := validValues[val]; ok {
sides |= n
} else {
return AllSides
}
}
return sides
} else if n, err := strconv.Atoi(value); err == nil {
if n >= 1 || n <= AllSides {
return n
}
} else if n, ok := validValues[value]; ok {
return n
}
}
case int:
if value >= 1 && value <= AllSides {
return value
}
}
}
return AllSides
}
func (resizable *resizableData) setSide(value interface{}) bool {
switch value := value.(type) {
case string:
if n, err := strconv.Atoi(value); err == nil {
if n >= 1 && n <= AllSides {
resizable.properties[Side] = n
return true
}
return false
}
validValues := map[string]int{
"top": TopSide,
"right": RightSide,
"bottom": BottomSide,
"left": LeftSide,
"all": AllSides,
}
if strings.Contains(value, "|") {
values := strings.Split(value, "|")
sides := 0
hasConst := false
for i, val := range values {
val := strings.Trim(val, " \t\r\n")
values[i] = val
if val[0] == '@' {
hasConst = true
} else if n, err := strconv.Atoi(val); err == nil {
if n < 1 || n > AllSides {
return false
}
sides |= n
} else if n, ok := validValues[val]; ok {
sides |= n
} else {
return false
}
}
if hasConst {
value = values[0]
for i := 1; i < len(values); i++ {
value += "|" + values[i]
}
resizable.properties[Side] = value
return true
}
if sides >= 1 && sides <= AllSides {
resizable.properties[Side] = sides
return true
}
} else if value[0] == '@' {
resizable.properties[Side] = value
return true
} else if n, ok := validValues[value]; ok {
resizable.properties[Side] = n
return true
}
case int:
if value >= 1 && value <= AllSides {
resizable.properties[Side] = value
return true
} else {
ErrorLogF(`Invalid value %d of "side" property`, value)
return false
}
default:
if n, ok := isInt(value); ok {
if n >= 1 && n <= AllSides {
resizable.properties[Side] = n
return true
} else {
ErrorLogF(`Invalid value %d of "side" property`, n)
return false
}
}
}
return false
}
func (resizable *resizableData) resizeBorderWidth() SizeUnit {
result, _ := sizeProperty(resizable, ResizeBorderWidth, resizable.Session())
if result.Type == Auto || result.Value == 0 {
return Px(4)
}
return result
}
func (resizable *resizableData) updateResizeBorderWidth() {
htmlID := resizable.htmlID()
session := resizable.Session()
column, row := resizable.cellSizeCSS()
updateCSSProperty(htmlID, "grid-template-columns", column, session)
updateCSSProperty(htmlID, "grid-template-rows", row, session)
}
func (resizable *resizableData) cellSizeCSS() (string, string) {
w := resizable.resizeBorderWidth().cssString("4px")
side := resizable.getSide()
column := "1fr"
row := "1fr"
if side&LeftSide != 0 {
if (side & RightSide) != 0 {
column = w + " 1fr " + w
} else {
column = w + " 1fr"
}
} else if (side & RightSide) != 0 {
column = "1fr " + w
}
if side&TopSide != 0 {
if (side & BottomSide) != 0 {
row = w + " 1fr " + w
} else {
row = w + " 1fr"
}
} else if (side & BottomSide) != 0 {
row = "1fr " + w
}
return column, row
}
func (resizable *resizableData) cssStyle(self View, builder cssBuilder) {
column, row := resizable.cellSizeCSS()
builder.add("grid-template-columns", column)
builder.add("grid-template-rows", row)
resizable.viewData.cssStyle(self, builder)
}
func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder) {
side := resizable.getSide()
left := 1
top := 1
leftSide := (side & LeftSide) != 0
rightSide := (side & RightSide) != 0
w := resizable.resizeBorderWidth().cssString("4px")
if leftSide {
left = 2
}
writePos := func(x1, x2, y1, y2 int) {
buffer.WriteString(fmt.Sprintf(` grid-column-start: %d; grid-column-end: %d; grid-row-start: %d; grid-row-end: %d;"></div>`, x1, x2, y1, y2))
}
//htmlID := resizable.htmlID()
if (side & TopSide) != 0 {
top = 2
if leftSide {
buffer.WriteString(`<div onmousedown="startResize(this, -1, -1, event)" style="cursor: nwse-resize; width: `)
buffer.WriteString(w)
buffer.WriteString(`; height: `)
buffer.WriteString(w)
buffer.WriteString(`;`)
writePos(1, 2, 1, 2)
}
buffer.WriteString(`<div onmousedown="startResize(this, 0, -1, event)" style="cursor: ns-resize; width: 100%; height: `)
buffer.WriteString(w)
buffer.WriteString(`;`)
writePos(left, left+1, 1, 2)
if rightSide {
buffer.WriteString(`<div onmousedown="startResize(this, 1, -1, event)" style="cursor: nesw-resize; width: `)
buffer.WriteString(w)
buffer.WriteString(`; height: `)
buffer.WriteString(w)
buffer.WriteString(`;`)
writePos(left+1, left+2, 1, 2)
}
}
if leftSide {
buffer.WriteString(`<div onmousedown="startResize(this, -1, 0, event)" style="cursor: ew-resize; width: `)
buffer.WriteString(w)
buffer.WriteString(`; height: 100%;`)
writePos(1, 2, top, top+1)
}
if rightSide {
buffer.WriteString(`<div onmousedown="startResize(this, 1, 0, event)" style="cursor: ew-resize; width: `)
buffer.WriteString(w)
buffer.WriteString(`; height: 100%;`)
writePos(left+1, left+2, top, top+1)
}
if (side & BottomSide) != 0 {
if leftSide {
buffer.WriteString(`<div onmousedown="startResize(this, -1, 1, event)" style="cursor: nesw-resize; width: `)
buffer.WriteString(w)
buffer.WriteString(`; height: `)
buffer.WriteString(w)
buffer.WriteString(`;`)
writePos(1, 2, top+1, top+2)
}
buffer.WriteString(`<div onmousedown="startResize(this, 0, 1, event)" style="cursor: ns-resize; width: 100%; height: `)
buffer.WriteString(w)
buffer.WriteString(`;`)
writePos(left, left+1, top+1, top+2)
if rightSide {
buffer.WriteString(`<div onmousedown="startResize(this, 1, 1, event)" style="cursor: nwse-resize; width: `)
buffer.WriteString(w)
buffer.WriteString(`; height: `)
buffer.WriteString(w)
buffer.WriteString(`;`)
writePos(left+1, left+2, top+1, top+2)
}
}
if len(resizable.content) > 0 {
view := resizable.content[0]
view.addToCSSStyle(map[string]string{
"grid-column-start": strconv.Itoa(left),
"grid-column-end": strconv.Itoa(left + 1),
"grid-row-start": strconv.Itoa(top),
"grid-row-end": strconv.Itoa(top + 1),
})
viewHTML(view, buffer)
}
}

216
resizeEvent.go Normal file
View File

@ -0,0 +1,216 @@
package rui
// ResizeEvent is the constant for "resize-event" property tag
// The "resize-event" is fired when the view changes its size.
// The main listener format: func(View, Frame).
// The additional listener formats: func(Frame), func(View), and func().
const ResizeEvent = "resize-event"
func (view *viewData) onResize(self View, x, y, width, height float64) {
view.frame.Left = x
view.frame.Top = y
view.frame.Width = width
view.frame.Height = height
for _, listener := range GetResizeListeners(view, "") {
listener(self, view.frame)
}
}
func (view *viewData) onItemResize(self View, index int, x, y, width, height float64) {
}
func (view *viewData) setFrameListener(tag string, value interface{}) bool {
if value == nil {
delete(view.properties, tag)
return true
}
switch value := value.(type) {
case func(View, Frame):
view.properties[tag] = []func(View, Frame){value}
case []func(View, Frame):
if len(value) > 0 {
view.properties[tag] = value
} else {
delete(view.properties, tag)
return true
}
case func(Frame):
fn := func(view View, frame Frame) {
value(frame)
}
view.properties[tag] = []func(View, Frame){fn}
case []func(Frame):
count := len(value)
if count == 0 {
delete(view.properties, tag)
return true
}
listeners := make([]func(View, Frame), count)
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
listeners[i] = func(view View, frame Frame) {
val(frame)
}
}
view.properties[tag] = listeners
case func(View):
fn := func(view View, frame Frame) {
value(view)
}
view.properties[tag] = []func(View, Frame){fn}
case []func(View):
count := len(value)
if count == 0 {
delete(view.properties, tag)
return true
}
listeners := make([]func(View, Frame), count)
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
listeners[i] = func(view View, frame Frame) {
val(view)
}
}
view.properties[tag] = listeners
case func():
fn := func(view View, frame Frame) {
value()
}
view.properties[tag] = []func(View, Frame){fn}
case []func():
count := len(value)
if count == 0 {
delete(view.properties, tag)
return true
}
listeners := make([]func(View, Frame), count)
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
listeners[i] = func(view View, frame Frame) {
val()
}
}
view.properties[tag] = listeners
case []interface{}:
count := len(value)
if count == 0 {
delete(view.properties, tag)
return true
}
listeners := make([]func(View, Frame), count)
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
switch val := val.(type) {
case func(View, Frame):
listeners[i] = val
case func(Frame):
listeners[i] = func(view View, frame Frame) {
val(frame)
}
case func(View):
listeners[i] = func(view View, frame Frame) {
val(view)
}
case func():
listeners[i] = func(view View, frame Frame) {
val()
}
default:
notCompatibleType(tag, val)
return false
}
}
view.properties[tag] = listeners
default:
notCompatibleType(tag, value)
return false
}
return true
}
func (view *viewData) setNoResizeEvent() {
view.noResizeEvent = true
}
func (view *viewData) isNoResizeEvent() bool {
return view.noResizeEvent
}
func (container *viewsContainerData) isNoResizeEvent() bool {
if container.noResizeEvent {
return true
}
if parent := container.Parent(); parent != nil {
return parent.isNoResizeEvent()
}
return false
}
func (view *viewData) Frame() Frame {
return view.frame
}
// GetViewFrame returns the size and location of view's viewport.
// If the second argument (subviewID) is "" then the value of the first argument (view) is returned
func GetViewFrame(view View, subviewID string) Frame {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return Frame{}
}
return view.Frame()
}
// GetResizeListeners returns the list of "resize-event" listeners. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is "" then the listeners list of the first argument (view) is returned
func GetResizeListeners(view View, subviewID string) []func(View, Frame) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(ResizeEvent); value != nil {
if result, ok := value.([]func(View, Frame)); ok {
return result
}
}
}
return []func(View, Frame){}
}

418
resources.go Normal file
View File

@ -0,0 +1,418 @@
package rui
import (
"embed"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
)
const (
imageDir = "images"
themeDir = "themes"
viewDir = "views"
rawDir = "raw"
stringsDir = "strings"
)
type scaledImage struct {
path string
scale float64
}
type imagePath struct {
path string
fs *embed.FS
}
type resourceManager struct {
embedFS []*embed.FS
themes map[string]*theme
images map[string]imagePath
imageSrcSets map[string][]scaledImage
path string
}
var resources = resourceManager{
embedFS: []*embed.FS{},
themes: map[string]*theme{},
images: map[string]imagePath{},
imageSrcSets: map[string][]scaledImage{},
}
func AddEmbedResources(fs *embed.FS) {
resources.embedFS = append(resources.embedFS, fs)
rootDirs := embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir:
scanEmbedImagesDir(fs, dir, "")
case themeDir:
scanEmbedThemesDir(fs, dir)
case stringsDir:
scanEmbedStringsDir(fs, dir)
case viewDir, rawDir:
// do nothing
default:
if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files {
if file.IsDir() {
switch file.Name() {
case imageDir:
scanEmbedImagesDir(fs, dir+"/"+imageDir, "")
case themeDir:
scanEmbedThemesDir(fs, dir+"/"+themeDir)
case stringsDir:
scanEmbedStringsDir(fs, dir+"/"+stringsDir)
case viewDir, rawDir:
// do nothing
}
}
}
}
}
}
}
func embedRootDirs(fs *embed.FS) []string {
result := []string{}
if files, err := fs.ReadDir("."); err == nil {
for _, file := range files {
if file.IsDir() {
result = append(result, file.Name())
}
}
}
return result
}
func scanEmbedThemesDir(fs *embed.FS, dir string) {
if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files {
name := file.Name()
path := dir + "/" + name
if file.IsDir() {
scanEmbedThemesDir(fs, path)
} else if strings.ToLower(filepath.Ext(name)) == ".rui" {
if data, err := fs.ReadFile(path); err == nil {
RegisterThemeText(string(data))
}
}
}
}
}
func scanEmbedImagesDir(fs *embed.FS, dir, prefix string) {
if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files {
name := file.Name()
path := dir + "/" + name
if file.IsDir() {
scanEmbedImagesDir(fs, path, prefix+name+"/")
} else {
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".png", ".jpg", ".jpeg", ".svg":
registerImage(fs, path, prefix+name)
}
}
}
}
}
func invalidImageFileFormat(filename string) {
ErrorLog(`Invalid image file name parameters: "` + filename +
`". Image file name format: name[@x-param].ext (examples: icon.png, icon@1.5x.png)`)
}
func registerImage(fs *embed.FS, path, filename string) {
resources.images[filename] = imagePath{fs: fs, path: path}
start := strings.LastIndex(filename, "@")
if start < 0 {
return
}
ext := strings.LastIndex(filename, ".")
if start > ext || filename[ext-1] != 'x' {
invalidImageFileFormat(path)
return
}
if scale, err := strconv.ParseFloat(filename[start+1:ext-1], 32); err == nil {
key := filename[:start] + filename[ext:]
images, ok := resources.imageSrcSets[key]
if ok {
for _, image := range images {
if image.scale == scale {
return
}
}
} else {
images = []scaledImage{}
}
resources.imageSrcSets[key] = append(images, scaledImage{path: filename, scale: scale})
} else {
invalidImageFileFormat(path)
return
}
}
func scanImagesDirectory(path, filePrefix string) {
if files, err := ioutil.ReadDir(path); err == nil {
for _, file := range files {
filename := file.Name()
if filename[0] != '.' {
newPath := path + `/` + filename
if !file.IsDir() {
registerImage(nil, newPath, filePrefix+filename)
} else {
scanImagesDirectory(newPath, filePrefix+filename+"/")
}
}
}
} else {
ErrorLog(err.Error())
}
}
func scanThemesDir(path string) {
if files, err := ioutil.ReadDir(path); err == nil {
for _, file := range files {
filename := file.Name()
if filename[0] != '.' {
newPath := path + `/` + filename
if file.IsDir() {
scanThemesDir(newPath)
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
if data, err := ioutil.ReadFile(newPath); err == nil {
RegisterThemeText(string(data))
} else {
ErrorLog(err.Error())
}
}
}
}
} else {
ErrorLog(err.Error())
}
}
// SetResourcePath set path of the resource directory
func SetResourcePath(path string) {
resources.path = path
pathLen := len(path)
if pathLen > 0 && path[pathLen-1] != '/' {
resources.path += "/"
}
scanImagesDirectory(resources.path+imageDir, "")
scanThemesDir(resources.path + themeDir)
scanStringsDir(resources.path + stringsDir)
}
// RegisterThemeText parse text and add result to the theme list
func RegisterThemeText(text string) bool {
data := ParseDataText(text)
if data == nil {
return false
}
if !data.IsObject() {
ErrorLog(`Root element is not object`)
return false
}
if data.Tag() != "theme" {
ErrorLog(`Invalid the root object tag. Must be "theme"`)
return false
}
if name, ok := data.PropertyValue("name"); ok && name != "" {
t := resources.themes[name]
if t == nil {
t = new(theme)
t.init()
resources.themes[name] = t
}
t.addData(data)
} else {
defaultTheme.addData(data)
}
return true
}
func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request) bool {
serveEmbed := func(fs *embed.FS, path string) bool {
if file, err := fs.Open(path); err == nil {
if stat, err := file.Stat(); err == nil {
http.ServeContent(w, r, filename, stat.ModTime(), file.(io.ReadSeeker))
return true
}
}
return false
}
if image, ok := resources.images[filename]; ok {
if image.fs != nil {
if serveEmbed(image.fs, image.path) {
return true
}
} else {
if _, err := os.Stat(image.path); err == nil {
http.ServeFile(w, r, image.path)
return true
}
}
}
for _, fs := range resources.embedFS {
if serveEmbed(fs, filename) {
return true
}
for _, dir := range embedRootDirs(fs) {
if serveEmbed(fs, dir+"/"+filename) {
return true
}
if subdirs, err := fs.ReadDir(dir); err == nil {
for _, subdir := range subdirs {
if subdir.IsDir() {
if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) {
return true
}
}
}
}
}
}
serve := func(path, filename string) bool {
filepath := path + filename
if _, err := os.Stat(filepath); err == nil {
http.ServeFile(w, r, filepath)
return true
}
filepath = path + imageDir + "/" + filename
if _, err := os.Stat(filepath); err == nil {
http.ServeFile(w, r, filepath)
return true
}
return false
}
if resources.path != "" && serve(resources.path, filename) {
return true
}
if exe, err := os.Executable(); err == nil {
path := filepath.Dir(exe) + "/resources/"
if serve(path, filename) {
return true
}
}
return false
}
func ReadRawResource(filename string) []byte {
for _, fs := range resources.embedFS {
rootDirs := embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, viewDir:
// do nothing
case rawDir:
if data, err := fs.ReadFile(dir + "/" + filename); err == nil {
return data
}
default:
if data, err := fs.ReadFile(dir + "/" + rawDir + "/" + filename); err == nil {
return data
}
}
}
}
readFile := func(path string) []byte {
if data, err := os.ReadFile(resources.path + rawDir + "/" + filename); err == nil {
return data
}
return nil
}
if resources.path != "" {
if data := readFile(resources.path + rawDir + "/" + filename); data != nil {
return data
}
}
if exe, err := os.Executable(); err == nil {
if data := readFile(filepath.Dir(exe) + "/resources/" + rawDir + "/" + filename); data != nil {
return data
}
}
ErrorLogF(`The raw file "%s" don't found`, filename)
return nil
}
func AllRawResources() []string {
result := []string{}
for _, fs := range resources.embedFS {
rootDirs := embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, viewDir:
// do nothing
case rawDir:
if files, err := fs.ReadDir(rawDir); err == nil {
for _, file := range files {
result = append(result, file.Name())
}
}
default:
if files, err := fs.ReadDir(dir + "/" + rawDir); err == nil {
for _, file := range files {
result = append(result, file.Name())
}
}
}
}
}
if resources.path != "" {
if files, err := ioutil.ReadDir(resources.path + rawDir); err == nil {
for _, file := range files {
result = append(result, file.Name())
}
}
}
if exe, err := os.Executable(); err == nil {
if files, err := ioutil.ReadDir(filepath.Dir(exe) + "/resources/" + rawDir); err == nil {
for _, file := range files {
result = append(result, file.Name())
}
}
}
return result
}

8
rui.code-workspace Normal file
View File

@ -0,0 +1,8 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

203
ruiWriter.go Normal file
View File

@ -0,0 +1,203 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
type ruiWriter interface {
startObject(tag string)
startObjectProperty(tag, objectTag string)
endObject()
writeProperty(tag string, value interface{})
finish() string
}
type ruiStringer interface {
ruiString(writer ruiWriter)
}
type ruiWriterData struct {
buffer *strings.Builder
indent string
}
func newRUIWriter() ruiWriter {
writer := new(ruiWriterData)
return writer
}
func (writer *ruiWriterData) writeIndent() {
if writer.buffer == nil {
writer.buffer = allocStringBuilder()
writer.indent = ""
return
}
if writer.indent != "" {
writer.buffer.WriteString(writer.indent)
}
}
func (writer *ruiWriterData) writeString(str string) {
esc := map[string]string{"\t": `\t`, "\r": `\r`, "\n": `\n`, "\"": `"`}
hasEsc := false
for s := range esc {
if strings.Contains(str, s) {
hasEsc = true
break
}
}
if hasEsc || strings.Contains(str, " ") || strings.Contains(str, ",") {
if !strings.Contains(str, "`") && (hasEsc || strings.Contains(str, `\`)) {
writer.buffer.WriteRune('`')
writer.buffer.WriteString(str)
writer.buffer.WriteRune('`')
} else {
str = strings.Replace(str, `\`, `\\`, -1)
for oldStr, newStr := range esc {
str = strings.Replace(str, oldStr, newStr, -1)
}
writer.buffer.WriteRune('"')
writer.buffer.WriteString(str)
writer.buffer.WriteRune('"')
}
} else {
writer.buffer.WriteString(str)
}
}
func (writer *ruiWriterData) startObject(tag string) {
writer.writeIndent()
writer.indent += "\t"
writer.writeString(tag)
writer.buffer.WriteString(" {\n")
}
func (writer *ruiWriterData) startObjectProperty(tag, objectTag string) {
writer.writeIndent()
writer.indent += "\t"
writer.writeString(tag)
writer.writeString(" = ")
writer.writeString(objectTag)
writer.buffer.WriteString(" {\n")
}
func (writer *ruiWriterData) endObject() {
if len(writer.indent) > 0 {
writer.indent = writer.indent[1:]
}
writer.writeIndent()
writer.buffer.WriteRune('}')
}
func (writer *ruiWriterData) writeValue(value interface{}) {
switch value := value.(type) {
case string:
writer.writeString(value)
case ruiStringer:
value.ruiString(writer)
// TODO
case fmt.Stringer:
writer.writeString(value.String())
case float32:
writer.writeString(fmt.Sprintf("%g", float64(value)))
case float64:
writer.writeString(fmt.Sprintf("%g", value))
case []string:
switch len(value) {
case 0:
writer.buffer.WriteString("[]\n")
case 1:
writer.writeString(value[0])
default:
writer.buffer.WriteString("[\n")
writer.indent += "\t"
for _, v := range value {
writer.buffer.WriteString(writer.indent)
writer.writeString(v)
writer.buffer.WriteString(",\n")
}
writer.indent = writer.indent[1:]
writer.buffer.WriteString(writer.indent)
writer.buffer.WriteRune(']')
}
case []View:
switch len(value) {
case 0:
writer.buffer.WriteString("[]\n")
case 1:
writer.writeValue(value[0])
default:
writer.buffer.WriteString("[\n")
writer.indent += "\t"
for _, v := range value {
writer.buffer.WriteString(writer.indent)
v.ruiString(writer)
writer.buffer.WriteString(",\n")
}
writer.indent = writer.indent[1:]
writer.buffer.WriteString(writer.indent)
writer.buffer.WriteRune(']')
}
case []interface{}:
switch len(value) {
case 0:
writer.buffer.WriteString("[]\n")
case 1:
writer.writeValue(value[0])
default:
writer.buffer.WriteString("[\n")
writer.indent += "\t"
for _, v := range value {
writer.buffer.WriteString(writer.indent)
writer.writeValue(v)
writer.buffer.WriteString(",\n")
}
writer.indent = writer.indent[1:]
writer.buffer.WriteString(writer.indent)
writer.buffer.WriteRune(']')
}
default:
if n, ok := isInt(value); ok {
writer.buffer.WriteString(strconv.Itoa(n))
}
}
writer.buffer.WriteString(",\n")
}
func (writer *ruiWriterData) writeProperty(tag string, value interface{}) {
writer.writeIndent()
writer.writeString(tag)
writer.buffer.WriteString(" = ")
writer.writeValue(value)
}
func (writer *ruiWriterData) finish() string {
result := ""
if writer.buffer != nil {
result = writer.buffer.String()
freeStringBuilder(writer.buffer)
writer.buffer = nil
}
return result
}

91
scrollEvent.go Normal file
View File

@ -0,0 +1,91 @@
package rui
import "fmt"
// ScrollEvent is the constant for "scroll-event" property tag
// The "resize-event" is fired when the content of the view is scrolled.
// The main listener format: func(View, Frame).
// The additional listener formats: func(Frame), func(View), and func().
const ScrollEvent = "scroll-event"
func (view *viewData) onScroll(self View, x, y, width, height float64) {
view.scroll.Left = x
view.scroll.Top = y
view.scroll.Width = width
view.scroll.Height = height
for _, listener := range GetScrollListeners(view, "") {
listener(self, view.scroll)
}
}
func (view *viewData) Scroll() Frame {
return view.scroll
}
func (view *viewData) setScroll(x, y, width, height float64) {
view.scroll.Left = x
view.scroll.Top = y
view.scroll.Width = width
view.scroll.Height = height
}
// GetViewScroll returns ...
// If the second argument (subviewID) is "" then a value of the first argument (view) is returned
func GetViewScroll(view View, subviewID string) Frame {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return Frame{}
}
return view.Scroll()
}
// GetScrollListeners returns the list of "scroll-event" listeners. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is "" then the listeners list of the first argument (view) is returned
func GetScrollListeners(view View, subviewID string) []func(View, Frame) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(ScrollEvent); value != nil {
if result, ok := value.([]func(View, Frame)); ok {
return result
}
}
}
return []func(View, Frame){}
}
// ScrollTo scrolls the view's content to the given position.
// If the second argument (subviewID) is "" then the first argument (view) is used
func ScrollViewTo(view View, subviewID string, x, y float64) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
view.Session().runScript(fmt.Sprintf(`scrollTo("%s", %g, %g)`, view.htmlID(), x, y))
}
}
// ScrollViewToEnd scrolls the view's content to the start of view.
// If the second argument (subviewID) is "" then the first argument (view) is used
func ScrollViewToStart(view View, subviewID string) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
view.Session().runScript(`scrollToStart("` + view.htmlID() + `")`)
}
}
// ScrollViewToEnd scrolls the view's content to the end of view.
// If the second argument (subviewID) is "" then the first argument (view) is used
func ScrollViewToEnd(view View, subviewID string) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
view.Session().runScript(`scrollToEnd("` + view.htmlID() + `")`)
}
}

401
session.go Normal file
View File

@ -0,0 +1,401 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
// SessionContent is the interface of a session content
type SessionContent interface {
CreateRootView(session Session) View
}
// Session provide interface to session parameters assess
type Session interface {
// App return the current application interface
App() Application
// ID return the id of the session
ID() int
// DarkTheme returns "true" if the dark theme is used
DarkTheme() bool
// Mobile returns "true" if current session is displayed on a touch screen device
TouchScreen() bool
// PixelRatio returns the ratio of the resolution in physical pixels to the resolution
// in logical pixels for the current display device.
PixelRatio() float64
// TextDirection returns the default text direction (LeftToRightDirection (1) or RightToLeftDirection (2))
TextDirection() int
// Constant returns the constant with "tag" name or "" if it is not exists
Constant(tag string) (string, bool)
// Color returns the color with "tag" name or 0 if it is not exists
Color(tag string) (Color, bool)
// SetCustomTheme set the custom theme
SetCustomTheme(name string) bool
// Language returns the current session language
Language() string
// SetLanguage set the current session language
SetLanguage(lang string)
// GetString returns the text for the current language
GetString(tag string) (string, bool)
Content() SessionContent
setContent(content SessionContent, self Session) bool
// RootView returns the root view of the session
RootView() View
// Get returns a value of the view (with id defined by the first argument) property with name defined by the second argument.
// The type of return value depends on the property. If the property is not set then nil is returned.
Get(viewID, tag string) interface{}
// Set sets the value (third argument) of the property (second argument) of the view with id defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log
Set(viewID, tag string, value interface{}) bool
resolveConstants(value string) (string, bool)
checkboxOffImage() string
checkboxOnImage() string
radiobuttonOffImage() string
radiobuttonOnImage() string
viewByHTMLID(id string) View
nextViewID() string
styleProperty(styleTag, property string) (string, bool)
stylePropertyNode(styleTag, propertyTag string) DataNode
setBrige(events chan DataObject, brige WebBrige)
writeInitScript(writer *strings.Builder)
runScript(script string)
runGetterScript(script string) DataObject //, answer chan DataObject)
handleAnswer(data DataObject)
handleResize(data DataObject)
handleViewEvent(command string, data DataObject)
close()
onStart()
onFinish()
onPause()
onResume()
onDisconnect()
onReconnect()
ignoreViewUpdates() bool
setIgnoreViewUpdates(ignore bool)
popupManager() *popupManager
imageManager() *imageManager
}
type sessionData struct {
customTheme *theme
darkTheme bool
touchScreen bool
textDirection int
pixelRatio float64
language string
languages []string
checkboxOff string
checkboxOn string
radiobuttonOff string
radiobuttonOn string
app Application
sessionID int
viewCounter int
content SessionContent
rootView View
ignoreUpdates bool
popups *popupManager
images *imageManager
brige WebBrige
events chan DataObject
}
func newSession(app Application, id int, customTheme string, params DataObject) Session {
session := new(sessionData)
session.app = app
session.sessionID = id
session.darkTheme = false
session.touchScreen = false
session.pixelRatio = 1
session.textDirection = LeftToRightDirection
session.languages = []string{}
session.viewCounter = 0
session.ignoreUpdates = false
if customTheme != "" {
if theme, ok := newTheme(customTheme); ok {
session.customTheme = theme
}
}
if value, ok := params.PropertyValue("touch"); ok {
session.touchScreen = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("direction"); ok {
if value == "rtl" {
session.textDirection = RightToLeftDirection
}
}
if value, ok := params.PropertyValue("languages"); ok {
session.languages = strings.Split(value, ",")
}
if value, ok := params.PropertyValue("dark"); ok {
session.darkTheme = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("pixel-ratio"); ok {
if f, err := strconv.ParseFloat(value, 64); err != nil {
ErrorLog(err.Error())
} else {
session.pixelRatio = f
}
}
return session
}
func (session *sessionData) App() Application {
return session.app
}
func (session *sessionData) ID() int {
return session.sessionID
}
func (session *sessionData) setBrige(events chan DataObject, brige WebBrige) {
session.events = events
session.brige = brige
}
func (session *sessionData) close() {
if session.events != nil {
session.events <- ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`)
}
}
func (session *sessionData) styleProperty(styleTag, propertyTag string) (string, bool) {
var style DataObject
ok := false
if session.customTheme != nil {
style, ok = session.customTheme.styles[styleTag]
}
if !ok {
style, ok = defaultTheme.styles[styleTag]
}
if ok {
if node := style.PropertyWithTag(propertyTag); node != nil && node.Type() == TextNode {
return session.resolveConstants(node.Text())
}
}
//errorLogF(`property "%v" not found`, propertyTag)
return "", false
}
func (session *sessionData) stylePropertyNode(styleTag, propertyTag string) DataNode {
var style DataObject
ok := false
if session.customTheme != nil {
style, ok = session.customTheme.styles[styleTag]
}
if !ok {
style, ok = defaultTheme.styles[styleTag]
}
if ok {
return style.PropertyWithTag(propertyTag)
}
return nil
}
func (session *sessionData) nextViewID() string {
session.viewCounter++
return fmt.Sprintf("id%06d", session.viewCounter)
}
func (session *sessionData) viewByHTMLID(id string) View {
if session.rootView == nil {
return nil
}
popupManager := session.popupManager()
for _, popup := range popupManager.popups {
if view := popup.viewByHTMLID(id); view != nil {
return view
}
}
return viewByHTMLID(id, session.rootView)
}
func (session *sessionData) Content() SessionContent {
return session.content
}
func (session *sessionData) setContent(content SessionContent, self Session) bool {
if content != nil {
session.content = content
session.rootView = content.CreateRootView(self)
if session.rootView != nil {
return true
}
}
return false
}
func (session *sessionData) RootView() View {
return session.rootView
}
func (session *sessionData) writeInitScript(writer *strings.Builder) {
var workTheme *theme
if session.customTheme == nil {
workTheme = defaultTheme
} else {
workTheme = new(theme)
workTheme.init()
workTheme.concat(defaultTheme)
workTheme.concat(session.customTheme)
}
if css := workTheme.cssText(session); css != "" {
writer.WriteString(`document.querySelector('style').textContent += "`)
writer.WriteString(css)
writer.WriteString("\";\n")
}
if session.rootView != nil {
writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
viewHTML(session.rootView, writer)
writer.WriteString("';\nscanElementsSize();")
}
}
func (session *sessionData) reload() {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
session.writeInitScript(buffer)
session.runScript(buffer.String())
}
func (session *sessionData) ignoreViewUpdates() bool {
return session.brige == nil || session.ignoreUpdates
}
func (session *sessionData) setIgnoreViewUpdates(ignore bool) {
session.ignoreUpdates = ignore
}
func (session *sessionData) Get(viewID, tag string) interface{} {
if view := ViewByID(session.RootView(), viewID); view != nil {
return view.Get(tag)
}
return nil
}
func (session *sessionData) Set(viewID, tag string, value interface{}) bool {
if view := ViewByID(session.RootView(), viewID); view != nil {
return view.Set(tag, value)
}
return false
}
func (session *sessionData) popupManager() *popupManager {
if session.popups == nil {
session.popups = new(popupManager)
session.popups.popups = []Popup{}
}
return session.popups
}
func (session *sessionData) imageManager() *imageManager {
if session.images == nil {
session.images = new(imageManager)
session.images.images = make(map[string]*imageData)
}
return session.images
}
func (session *sessionData) runScript(script string) {
if session.brige != nil {
session.brige.WriteMessage(script)
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) {
if session.brige != nil {
return session.brige.RunGetterScript(script)
}
ErrorLog("No connection")
result := NewDataObject("error")
result.SetPropertyValue("text", "No connection")
return result
}
func (session *sessionData) handleAnswer(data DataObject) {
session.brige.AnswerReceived(data)
}
func (session *sessionData) handleResize(data DataObject) {
if node := data.PropertyWithTag("views"); node != nil && node.Type() == ArrayNode {
for _, el := range node.ArrayElements() {
if el.IsObject() {
obj := el.Object()
getFloat := func(tag string) float64 {
if value, ok := obj.PropertyValue(tag); ok {
f, err := strconv.ParseFloat(value, 64)
if err == nil {
return f
}
ErrorLog(`Resize event error: ` + err.Error())
} else {
ErrorLogF(`Resize event error: the property "%s" not found`, tag)
}
return 0
}
if viewID, ok := obj.PropertyValue("id"); ok {
if n := strings.IndexRune(viewID, '-'); n > 0 {
if index, err := strconv.Atoi(viewID[n+1:]); err == nil {
if view := session.viewByHTMLID(viewID[:n]); view != nil {
view.onItemResize(view, index, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height"))
} else {
ErrorLogF(`View with id == %s not found`, viewID[:n])
}
} else {
ErrorLogF(`Invalid view id == %s not found`, viewID)
}
} else if view := session.viewByHTMLID(viewID); view != nil {
view.onResize(view, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height"))
view.setScroll(getFloat("scroll-x"), getFloat("scroll-y"), getFloat("scroll-width"), getFloat("scroll-height"))
} else {
ErrorLogF(`View with id == %s not found`, viewID)
}
} else {
ErrorLog(`"id" property not found`)
}
} else {
ErrorLog(`Resize event error: views element is not object`)
}
}
} else {
ErrorLog(`Resize event error: invalid "views" property`)
}
}
func (session *sessionData) handleViewEvent(command string, data DataObject) {
if viewID, ok := data.PropertyValue("id"); ok {
if view := session.viewByHTMLID(viewID); view != nil {
view.handleCommand(view, command, data)
}
} else {
ErrorLog(`"id" property not found. Event: ` + command)
}
}

81
sessionEvents.go Normal file
View File

@ -0,0 +1,81 @@
package rui
// SessionStartListener is the listener interface of a session start event
type SessionStartListener interface {
OnStart(session Session)
}
// SessionFinishListener is the listener interface of a session start event
type SessionFinishListener interface {
OnFinish(session Session)
}
// SessionResumeListener is the listener interface of a session resume event
type SessionResumeListener interface {
OnResume(session Session)
}
// SessionPauseListener is the listener interface of a session pause event
type SessionPauseListener interface {
OnPause(session Session)
}
// SessionPauseListener is the listener interface of a session disconnect event
type SessionDisconnectListener interface {
OnDisconnect(session Session)
}
// SessionPauseListener is the listener interface of a session reconnect event
type SessionReconnectListener interface {
OnReconnect(session Session)
}
func (session *sessionData) onStart() {
if session.content != nil {
if listener, ok := session.content.(SessionStartListener); ok {
listener.OnStart(session)
}
session.onResume()
}
}
func (session *sessionData) onFinish() {
if session.content != nil {
session.onPause()
if listener, ok := session.content.(SessionFinishListener); ok {
listener.OnFinish(session)
}
}
}
func (session *sessionData) onPause() {
if session.content != nil {
if listener, ok := session.content.(SessionPauseListener); ok {
listener.OnPause(session)
}
}
}
func (session *sessionData) onResume() {
if session.content != nil {
if listener, ok := session.content.(SessionResumeListener); ok {
listener.OnResume(session)
}
}
}
func (session *sessionData) onDisconnect() {
if session.content != nil {
if listener, ok := session.content.(SessionDisconnectListener); ok {
listener.OnDisconnect(session)
}
}
}
func (session *sessionData) onReconnect() {
if session.content != nil {
if listener, ok := session.content.(SessionReconnectListener); ok {
listener.OnReconnect(session)
}
}
}

359
sessionTheme.go Normal file
View File

@ -0,0 +1,359 @@
package rui
import (
"fmt"
"strings"
)
/*
type Session struct {
customTheme *theme
darkTheme bool
touchScreen bool
textDirection int
pixelRatio float64
language string
languages []string
checkboxOff string
checkboxOn string
radiobuttonOff string
radiobuttonOn string
}
*/
func (session *sessionData) DarkTheme() bool {
return session.darkTheme
}
func (session *sessionData) TouchScreen() bool {
return session.touchScreen
}
func (session *sessionData) PixelRatio() float64 {
return session.pixelRatio
}
func (session *sessionData) TextDirection() int {
return session.textDirection
}
func (session *sessionData) constant(tag string, prevTags []string) (string, bool) {
tags := append(prevTags, tag)
result := ""
themes := session.themes()
for {
ok := false
if session.touchScreen {
for _, theme := range themes {
if theme.touchConstants != nil {
if result, ok = theme.touchConstants[tag]; ok {
break
}
}
}
}
if !ok {
for _, theme := range themes {
if theme.constants != nil {
if result, ok = theme.constants[tag]; ok {
break
}
}
}
}
if !ok {
ErrorLogF(`"%v" constant not found`, tag)
return "", false
}
if len(result) < 2 || !strings.ContainsRune(result, '@') {
return result, true
}
for _, separator := range []string{",", " ", ":", ";", "|", "/"} {
if strings.Contains(result, separator) {
result, ok = session.resolveConstantsNext(result, tags)
return result, ok
}
}
if result[0] != '@' {
return result, true
}
tag = result[1:]
for _, t := range tags {
if t == tag {
ErrorLogF(`"%v" constant is cyclic`, tag)
return "", false
}
}
tags = append(tags, tag)
}
}
func (session *sessionData) resolveConstants(value string) (string, bool) {
return session.resolveConstantsNext(value, []string{})
}
func (session *sessionData) resolveConstantsNext(value string, prevTags []string) (string, bool) {
if !strings.Contains(value, "@") {
return value, true
}
separators := []rune{',', ' ', ':', ';', '|', '/'}
sep := rune(0)
index := -1
for _, s := range separators {
if i := strings.IndexRune(value, s); i >= 0 {
if i < index || index < 0 {
sep = s
index = i
}
}
}
ok := true
if index >= 0 {
v1 := strings.Trim(value[:index], " \t\n\r")
v2 := strings.Trim(value[index+1:], " \t\n\r")
if len(v1) > 1 && v1[0] == '@' {
if v1, ok = session.constant(v1[1:], prevTags); !ok {
return value, false
}
if v, ok := session.resolveConstantsNext(v1, prevTags); ok {
v1 = v
} else {
return v1 + string(sep) + v2, false
}
}
if v, ok := session.resolveConstantsNext(v2, prevTags); ok {
v2 = v
}
return v1 + string(sep) + v2, ok
} else if value[0] == '@' {
if value, ok = session.constant(value[1:], prevTags); ok {
return session.resolveConstantsNext(value, prevTags)
}
}
return value, false
}
func (session *sessionData) Constant(tag string) (string, bool) {
return session.constant(tag, []string{})
}
func (session *sessionData) themes() []*theme {
if session.customTheme != nil {
return []*theme{session.customTheme, defaultTheme}
}
return []*theme{defaultTheme}
}
// Color return the color with "tag" name or 0 if it is not exists
func (session *sessionData) Color(tag string) (Color, bool) {
tags := []string{tag}
result := ""
themes := session.themes()
for {
ok := false
if session.darkTheme {
for _, theme := range themes {
if theme.darkColors != nil {
if result, ok = theme.darkColors[tag]; ok {
break
}
}
}
}
if !ok {
for _, theme := range themes {
if theme.colors != nil {
if result, ok = theme.colors[tag]; ok {
break
}
}
}
}
if !ok {
ErrorLogF(`"%v" color not found`, tag)
return 0, false
}
if len(result) == 0 || result[0] != '@' {
color, ok := StringToColor(result)
if !ok {
ErrorLogF(`invalid value "%v" of "%v" color constant`, result, tag)
return 0, false
}
return color, true
}
tag = result[1:]
for _, t := range tags {
if t == tag {
ErrorLogF(`"%v" color is cyclic`, tag)
return 0, false
}
}
tags = append(tags, tag)
}
}
func (session *sessionData) SetCustomTheme(name string) bool {
if name == "" {
if session.customTheme == nil {
return true
}
} else if theme, ok := resources.themes[name]; ok {
session.customTheme = theme
} else {
return false
}
session.reload()
return true
}
const checkImage = `<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m4 8 3 4 5-8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"/></svg>`
func (session *sessionData) checkboxImage(checked bool) string {
var borderColor, backgroundColor Color
var ok bool
if borderColor, ok = session.Color("ruiDisabledTextColor"); !ok {
if session.darkTheme {
borderColor = 0xFFA0A0A0
} else {
borderColor = 0xFF202020
}
}
if checked {
if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
backgroundColor = 0xFF1A74E8
}
} else if backgroundColor, ok = session.Color("backgroundColor"); !ok {
if session.darkTheme {
backgroundColor = 0xFFA0A0A0
} else {
backgroundColor = 0xFF202020
}
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<div style="width: 18px; height: 18px; background-color: `)
buffer.WriteString(backgroundColor.cssString())
buffer.WriteString(`; border: 1px solid `)
buffer.WriteString(borderColor.cssString())
buffer.WriteString(`; border-radius: 4px;">`)
if checked {
buffer.WriteString(checkImage)
}
buffer.WriteString(`</div>`)
return buffer.String()
}
func (session *sessionData) checkboxOffImage() string {
if session.checkboxOff == "" {
session.checkboxOff = session.checkboxImage(false)
}
return session.checkboxOff
}
func (session *sessionData) checkboxOnImage() string {
if session.checkboxOn == "" {
session.checkboxOn = session.checkboxImage(true)
}
return session.checkboxOn
}
func (session *sessionData) radiobuttonOffImage() string {
if session.radiobuttonOff == "" {
var borderColor, backgroundColor Color
var ok bool
if borderColor, ok = session.Color("ruiDisabledTextColor"); !ok {
if session.darkTheme {
borderColor = 0xFFA0A0A0
} else {
borderColor = 0xFF202020
}
}
if backgroundColor, ok = session.Color("backgroundColor"); !ok {
if session.darkTheme {
backgroundColor = 0xFFA0A0A0
} else {
backgroundColor = 0xFF202020
}
}
session.radiobuttonOff = fmt.Sprintf(`<div style="width: 16px; height: 16px; background-color: %s; border: 1px solid %s; border-radius: 8px;"></div>`,
backgroundColor.cssString(), borderColor.cssString())
}
return session.radiobuttonOff
}
func (session *sessionData) radiobuttonOnImage() string {
if session.radiobuttonOn == "" {
var borderColor, backgroundColor Color
var ok bool
if borderColor, ok = session.Color("ruiHighlightColor"); !ok {
borderColor = 0xFF1A74E8
}
if backgroundColor, ok = session.Color("ruiHighlightTextColor"); !ok {
backgroundColor = 0xFFFFFFFF
}
session.radiobuttonOn = fmt.Sprintf(`<div style="width: 16px; height: 16px; display: grid; justify-items: center; align-items: center; background-color: %s; border: 2px solid %s; border-radius: 8px;"><div style="width: 8px; height: 8px; background-color: %s; border-radius: 4px;"></div></div>`,
backgroundColor.cssString(), borderColor.cssString(), borderColor.cssString())
}
return session.radiobuttonOn
}
func (session *sessionData) Language() string {
if session.language != "" {
return session.language
}
if session.languages != nil && len(session.languages) > 0 {
return session.languages[0]
}
return "en"
}
func (session *sessionData) SetLanguage(lang string) {
lang = strings.Trim(lang, " \t\n\r")
if lang != session.language {
session.language = lang
if session.rootView != nil {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
viewHTML(session.rootView, buffer)
buffer.WriteString("';\nscanElementsSize();")
session.runScript(buffer.String())
}
}
}

109
sessionUtils.go Normal file
View File

@ -0,0 +1,109 @@
package rui
import (
"fmt"
)
func sizeConstant(session Session, tag string) (SizeUnit, bool) {
if text, ok := session.Constant(tag); ok {
return StringToSizeUnit(text)
}
return AutoSize(), false
}
func updateCSSStyle(htmlID string, session Session) {
if !session.ignoreViewUpdates() {
if view := session.viewByHTMLID(htmlID); view != nil {
var builder viewCSSBuilder
builder.buffer = allocStringBuilder()
builder.buffer.WriteString(`updateCSSStyle('`)
builder.buffer.WriteString(view.htmlID())
builder.buffer.WriteString(`', '`)
view.cssStyle(view, &builder)
builder.buffer.WriteString(`');`)
view.Session().runScript(builder.finish())
}
}
}
func updateInnerHTML(htmlID string, session Session) {
if !session.ignoreViewUpdates() {
if view := session.viewByHTMLID(htmlID); view != nil {
script := allocStringBuilder()
defer freeStringBuilder(script)
script.Grow(32 * 1024)
view.htmlSubviews(view, script)
view.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, view.htmlID(), script.String()))
//view.updateEventHandlers()
}
}
}
func appendToInnerHTML(htmlID, content string, session Session) {
if !session.ignoreViewUpdates() {
if view := session.viewByHTMLID(htmlID); view != nil {
view.Session().runScript(fmt.Sprintf(`appendToInnerHTML('%v', '%v');`, view.htmlID(), content))
//view.updateEventHandlers()
}
}
}
func updateProperty(htmlID, property, value string, session Session) {
if !session.ignoreViewUpdates() {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', '%v');`, htmlID, property, value))
}
}
func updateCSSProperty(htmlID, property, value string, session Session) {
if !session.ignoreViewUpdates() {
session.runScript(fmt.Sprintf(`updateCSSProperty('%v', '%v', '%v');`, htmlID, property, value))
}
}
func updateBoolProperty(htmlID, property string, value bool, session Session) {
if !session.ignoreViewUpdates() {
if value {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', true);`, htmlID, property))
} else {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', false);`, htmlID, property))
}
}
}
func removeProperty(htmlID, property string, session Session) {
if !session.ignoreViewUpdates() {
session.runScript(fmt.Sprintf(`removeProperty('%v', '%v');`, htmlID, property))
}
}
/*
func setDisabled(htmlID string, disabled bool, session Session) {
if !session.ignoreViewUpdates() {
if disabled {
session.runScript(fmt.Sprintf(`setDisabled('%v', true);`, htmlID))
} else {
session.runScript(fmt.Sprintf(`setDisabled('%v', false);`, htmlID))
}
}
}
*/
func viewByHTMLID(id string, startView View) View {
if startView != nil {
if startView.htmlID() == id {
return startView
}
if container, ok := startView.(ParanetView); ok {
for _, view := range container.Views() {
if view != nil {
if v := viewByHTMLID(id, view); v != nil {
return v
}
}
}
}
}
return nil
}

122
session_test.go Normal file
View File

@ -0,0 +1,122 @@
package rui
import (
"testing"
)
var stopTestLogFlag = false
var testLogDone chan int
var ignoreTestLog = false
func createTestLog(t *testing.T, ignore bool) {
ignoreTestLog = ignore
SetErrorLog(func(text string) {
if ignoreTestLog {
t.Log(text)
} else {
t.Error(text)
}
})
SetDebugLog(func(text string) {
t.Log(text)
})
}
/*
func createTestSession(t *testing.T) *sessionData {
session := new(sessionData)
createTestLog(t, false)
return session
}
func TestSessionConstants(t *testing.T) {
session := createTestSession(t)
customTheme := `
theme {
colors = _{
textColor = #FF080808,
myColor = #81234567
},
colors:dark = _{
textColor = #FFF0F0F0,
myColor = #87654321
},
constants = _{
defaultPadding = 10px,
myConstant = 100%
const1 = "@const2, 10px; @const3"
const2 = "20mm / @const4"
const3 = "@const5 : 30pt"
const4 = "40%"
const5 = "50px"
},
constants:touch = _{
defaultPadding = 20px,
myConstant = 80%,
},
}
`
SetErrorLog(func(text string) {
t.Error(text)
})
theme, ok := newTheme(customTheme)
if !ok {
return
}
session.SetCustomTheme(theme)
type constPair struct {
tag, value string
}
testConstants := func(constants []constPair) {
for _, constant := range constants {
if value, ok := session.Constant(constant.tag); ok {
if value != constant.value {
t.Error(constant.tag + " = " + value + ". Need: " + constant.value)
}
}
}
}
testConstants([]constPair{
{tag: "defaultPadding", value: "10px"},
{tag: "myConstant", value: "100%"},
{tag: "buttonMargin", value: "4px"},
})
session.SetConstant("myConstant", "25px")
testConstants([]constPair{
{tag: "defaultPadding", value: "10px"},
{tag: "myConstant", value: "25px"},
{tag: "buttonMargin", value: "4px"},
})
session.touchScreen = true
testConstants([]constPair{
{tag: "defaultPadding", value: "20px"},
{tag: "myConstant", value: "80%"},
{tag: "buttonMargin", value: "4px"},
})
session.SetTouchConstant("myConstant", "30pt")
testConstants([]constPair{
{tag: "defaultPadding", value: "20px"},
{tag: "myConstant", value: "30pt"},
{tag: "buttonMargin", value: "4px"},
})
if value, ok := session.Constant("const1"); ok {
if value != "20mm/40%,10px;50px:30pt" {
t.Error("const1 = " + value + ". Need: 20mm/40%,10px;50px:30pt")
}
}
}
*/

312
shadow.go Normal file
View File

@ -0,0 +1,312 @@
package rui
import (
"fmt"
"strings"
)
const (
// ColorProperty is the name of the color property of the shadow.
ColorProperty = "color"
// Inset is the name of bool property of the shadow. If it is set to "false" (default) then the shadow
// is assumed to be a drop shadow (as if the box were raised above the content).
// If it is set to "true" then the shadow to one inside the frame (as if the content was depressed inside the box).
// Inset shadows are drawn inside the border (even transparent ones), above the background, but below content.
Inset = "inset"
// XOffset is the name of the SizeUnit property of the shadow that determines the shadow horizontal offset.
// Negative values place the shadow to the left of the element.
XOffset = "x-offset"
// YOffset is the name of the SizeUnit property of the shadow that determines the shadow vertical offset.
// Negative values place the shadow above the element.
YOffset = "y-offset"
// BlurRadius is the name of the SizeUnit property of the shadow that determines the radius of the blur effect.
// The larger this value, the bigger the blur, so the shadow becomes bigger and lighter. Negative values are not allowed.
BlurRadius = "blur"
// SpreadRadius is the name of the SizeUnit property of the shadow. Positive values will cause the shadow to expand
// and grow bigger, negative values will cause the shadow to shrink.
SpreadRadius = "spread-radius"
)
// ViewShadow contains attributes of the view shadow
type ViewShadow interface {
Properties
fmt.Stringer
ruiStringer
cssStyle(buffer *strings.Builder, session Session, lead string) bool
cssTextStyle(buffer *strings.Builder, session Session, lead string) bool
visible(session Session) bool
}
type viewShadowData struct {
propertyList
}
// NewViewShadow create the new shadow for a view. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// spreadRadius - the spread radius of the shadow
// color - the color of the shadow
func NewViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
BlurRadius: blurRadius,
SpreadRadius: spreadRadius,
ColorProperty: color,
})
}
// NewInsetViewShadow create the new inset shadow for a view. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// spreadRadius - the spread radius of the shadow
// color - the color of the shadow
func NewInsetViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
BlurRadius: blurRadius,
SpreadRadius: spreadRadius,
ColorProperty: color,
Inset: true,
})
}
// NewTextShadow create the new text shadow. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// color - the color of the shadow
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
BlurRadius: blurRadius,
ColorProperty: color,
})
}
// NewShadowWithParams create the new shadow for a view.
func NewShadowWithParams(params Params) ViewShadow {
shadow := new(viewShadowData)
shadow.propertyList.init()
if params != nil {
for _, tag := range []string{ColorProperty, Inset, XOffset, YOffset, BlurRadius, SpreadRadius} {
if value, ok := params[tag]; ok && value != nil {
shadow.Set(tag, value)
}
}
}
return shadow
}
// parseViewShadow parse DataObject and create ViewShadow object
func parseViewShadow(object DataObject) ViewShadow {
shadow := new(viewShadowData)
shadow.propertyList.init()
parseProperties(shadow, object)
return shadow
}
func (shadow *viewShadowData) Remove(tag string) {
delete(shadow.properties, strings.ToLower(tag))
}
func (shadow *viewShadowData) Set(tag string, value interface{}) bool {
if value == nil {
shadow.Remove(tag)
return true
}
tag = strings.ToLower(tag)
switch tag {
case ColorProperty, Inset, XOffset, YOffset, BlurRadius, SpreadRadius:
return shadow.propertyList.Set(tag, value)
}
ErrorLogF(`"%s" property is not supported by Shadow`, tag)
return false
}
func (shadow *viewShadowData) Get(tag string) interface{} {
return shadow.propertyList.Get(strings.ToLower(tag))
}
func (shadow *viewShadowData) cssStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorProperty, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
spreadRadius, _ := sizeProperty(shadow, SpreadRadius, session)
if color.Alpha() == 0 ||
((offsetX.Type == Auto || offsetX.Value == 0) &&
(offsetY.Type == Auto || offsetY.Value == 0) &&
(blurRadius.Type == Auto || blurRadius.Value == 0) &&
(spreadRadius.Type == Auto || spreadRadius.Value == 0)) {
return false
}
buffer.WriteString(lead)
if inset, _ := boolProperty(shadow, Inset, session); inset {
buffer.WriteString("inset ")
}
buffer.WriteString(offsetX.cssString("0"))
buffer.WriteByte(' ')
buffer.WriteString(offsetY.cssString("0"))
buffer.WriteByte(' ')
buffer.WriteString(blurRadius.cssString("0"))
buffer.WriteByte(' ')
buffer.WriteString(spreadRadius.cssString("0"))
buffer.WriteByte(' ')
buffer.WriteString(color.cssString())
return true
}
func (shadow *viewShadowData) cssTextStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorProperty, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
if color.Alpha() == 0 ||
((offsetX.Type == Auto || offsetX.Value == 0) &&
(offsetY.Type == Auto || offsetY.Value == 0) &&
(blurRadius.Type == Auto || blurRadius.Value == 0)) {
return false
}
buffer.WriteString(lead)
buffer.WriteString(offsetX.cssString("0"))
buffer.WriteByte(' ')
buffer.WriteString(offsetY.cssString("0"))
buffer.WriteByte(' ')
buffer.WriteString(blurRadius.cssString("0"))
buffer.WriteByte(' ')
buffer.WriteString(color.cssString())
return true
}
func (shadow *viewShadowData) visible(session Session) bool {
color, _ := colorProperty(shadow, ColorProperty, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
blurRadius, _ := sizeProperty(shadow, BlurRadius, session)
spreadRadius, _ := sizeProperty(shadow, SpreadRadius, session)
if color.Alpha() == 0 ||
((offsetX.Type == Auto || offsetX.Value == 0) &&
(offsetY.Type == Auto || offsetY.Value == 0) &&
(blurRadius.Type == Auto || blurRadius.Value == 0) &&
(spreadRadius.Type == Auto || spreadRadius.Value == 0)) {
return false
}
return true
}
func (shadow *viewShadowData) String() string {
writer := newRUIWriter()
shadow.ruiString(writer)
return writer.finish()
}
func (shadow *viewShadowData) ruiString(writer ruiWriter) {
writer.startObject("_")
for _, tag := range shadow.AllTags() {
if value := shadow.Get(tag); value != nil {
writer.writeProperty(tag, value)
}
}
writer.endObject()
}
func (properties *propertyList) setShadow(tag string, value interface{}) bool {
if value == nil {
delete(properties.properties, tag)
return true
}
switch value := value.(type) {
case ViewShadow:
properties.properties[tag] = []ViewShadow{value}
case []ViewShadow:
if len(value) == 0 {
delete(properties.properties, tag)
} else {
properties.properties[tag] = value
}
case DataValue:
if !value.IsObject() {
return false
}
properties.properties[tag] = []ViewShadow{parseViewShadow(value.Object())}
case []DataValue:
shadows := []ViewShadow{}
for _, data := range value {
if data.IsObject() {
shadows = append(shadows, parseViewShadow(data.Object()))
}
}
if len(shadows) == 0 {
return false
}
properties.properties[tag] = shadows
case string:
obj := NewDataObject(value)
if obj == nil {
notCompatibleType(tag, value)
return false
}
properties.properties[tag] = []ViewShadow{parseViewShadow(obj)}
default:
notCompatibleType(tag, value)
return false
}
return true
}
func getShadows(properties Properties, tag string) []ViewShadow {
if value := properties.Get(tag); value != nil {
switch value := value.(type) {
case []ViewShadow:
return value
case ViewShadow:
return []ViewShadow{value}
}
}
return []ViewShadow{}
}
func shadowCSS(properties Properties, tag string, session Session) string {
shadows := getShadows(properties, tag)
if len(shadows) == 0 {
return ""
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
lead := ""
if tag == Shadow {
for _, shadow := range shadows {
if shadow.cssStyle(buffer, session, lead) {
lead = ", "
}
}
} else {
for _, shadow := range shadows {
if shadow.cssTextStyle(buffer, session, lead) {
lead = ", "
}
}
}
return buffer.String()
}

177
sizeUnit.go Normal file
View File

@ -0,0 +1,177 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
// SizeUnitType : type of enumerated constants for define a type of SizeUnit value.
//
// Can take the following values: Auto, SizeInPixel, SizeInPercent,
// SizeInDIP, SizeInPt, SizeInInch, SizeInMM, SizeInFraction
type SizeUnitType uint8
const (
// Auto - default value.
Auto SizeUnitType = 0
// SizeInPixel - size in pixels.
SizeInPixel SizeUnitType = 1
// SizeInEM - size in em.
SizeInEM SizeUnitType = 2
// SizeInEX - size in em.
SizeInEX SizeUnitType = 3
// SizeInPercent - size in percents of a parant size.
SizeInPercent SizeUnitType = 4
// SizeInPt - size in pt (1/72 inch).
SizeInPt SizeUnitType = 5
// SizeInPc - size in pc (1pc = 12pt).
SizeInPc SizeUnitType = 6
// SizeInInch - size in inches.
SizeInInch SizeUnitType = 7
// SizeInMM - size in millimeters.
SizeInMM SizeUnitType = 8
// SizeInCM - size in centimeters.
SizeInCM SizeUnitType = 9
// SizeInFraction - size in fraction. Used only for "cell-width" and "cell-height" property
SizeInFraction SizeUnitType = 10
)
// SizeUnit describe a size (Value field) and size unit (Type field).
type SizeUnit struct {
Type SizeUnitType
Value float64
}
// AutoSize creates SizeUnit with Auto type
func AutoSize() SizeUnit {
return SizeUnit{Auto, 0}
}
// Px creates SizeUnit with SizeInPixel type
func Px(value float64) SizeUnit {
return SizeUnit{SizeInPixel, value}
}
// Em creates SizeUnit with SizeInEM type
func Em(value float64) SizeUnit {
return SizeUnit{SizeInEM, value}
}
// Ex creates SizeUnit with SizeInEX type
func Ex(value float64) SizeUnit {
return SizeUnit{SizeInEX, value}
}
// Percent creates SizeUnit with SizeInDIP type
func Percent(value float64) SizeUnit {
return SizeUnit{SizeInPercent, value}
}
// Pt creates SizeUnit with SizeInPt type
func Pt(value float64) SizeUnit {
return SizeUnit{SizeInPt, value}
}
// Pc creates SizeUnit with SizeInPc type
func Pc(value float64) SizeUnit {
return SizeUnit{SizeInPc, value}
}
// Mm creates SizeUnit with SizeInMM type
func Mm(value float64) SizeUnit {
return SizeUnit{SizeInMM, value}
}
// Cm creates SizeUnit with SizeInCM type
func Cm(value float64) SizeUnit {
return SizeUnit{SizeInCM, value}
}
// Inch creates SizeUnit with SizeInInch type
func Inch(value float64) SizeUnit {
return SizeUnit{SizeInInch, value}
}
// Fr creates SizeUnit with SizeInFraction type
func Fr(value float64) SizeUnit {
return SizeUnit{SizeInFraction, value}
}
// Equal compare two SizeUnit. Return true if SizeUnit are equal
func (size SizeUnit) Equal(size2 SizeUnit) bool {
return size.Type == size2.Type && (size.Type == Auto || size.Value == size2.Value)
}
func sizeUnitSuffixes() map[SizeUnitType]string {
return map[SizeUnitType]string{
SizeInPixel: "px",
SizeInPercent: "%",
SizeInEM: "em",
SizeInEX: "ex",
SizeInPt: "pt",
SizeInPc: "pc",
SizeInInch: "in",
SizeInMM: "mm",
SizeInCM: "cm",
SizeInFraction: "fr",
}
}
// StringToSizeUnit converts the string argument to SizeUnit
func StringToSizeUnit(value string) (SizeUnit, bool) {
value = strings.Trim(value, " \t\n\r")
switch value {
case "auto", "none", "":
return SizeUnit{Type: Auto, Value: 0}, true
case "0":
return SizeUnit{Type: SizeInPixel, Value: 0}, true
}
suffixes := sizeUnitSuffixes()
for unitType, suffix := range suffixes {
if strings.HasSuffix(value, suffix) {
var err error
var val float64
if val, err = strconv.ParseFloat(value[:len(value)-len(suffix)], 64); err != nil {
ErrorLog(err.Error())
return SizeUnit{Type: Auto, Value: 0}, false
}
return SizeUnit{Type: unitType, Value: val}, true
}
}
ErrorLog(`Invalid SizeUnit value: "` + value + `"`)
return SizeUnit{Type: Auto, Value: 0}, false
}
// String - convert SizeUnit to string
func (size SizeUnit) String() string {
if size.Type == Auto {
return "auto"
}
if suffix, ok := sizeUnitSuffixes()[size.Type]; ok {
return fmt.Sprintf("%g%s", size.Value, suffix)
}
return strconv.FormatFloat(size.Value, 'g', -1, 64)
}
// cssString - convert SizeUnit to string
func (size SizeUnit) cssString(textForAuto string) string {
switch size.Type {
case Auto:
return textForAuto
case SizeInEM:
return fmt.Sprintf("%grem", size.Value)
}
if size.Value == 0 {
return "0"
}
return size.String()
}

124
sizeUnit_test.go Normal file
View File

@ -0,0 +1,124 @@
package rui
/*
import (
"testing"
)
func TestSizeUnitNew(t *testing.T) {
_ = createTestSession(t)
size := SizeUnit{SizeInPixel, 10}
if Px(10) != size {
t.Error("Px(10) error")
}
size = SizeUnit{SizeInPercent, 10}
if Percent(10) != size {
t.Error("Percent(10) error")
}
size = SizeUnit{SizeInPt, 10}
if Pt(10) != size {
t.Error("Pt(10) error")
}
size = SizeUnit{SizeInCM, 10}
if Cm(10) != size {
t.Error("Dip(10) error")
}
size = SizeUnit{SizeInMM, 10}
if Mm(10) != size {
t.Error("Mm(10) error")
}
size = SizeUnit{SizeInInch, 10}
if Inch(10) != size {
t.Error("Inch(10) error")
}
}
func TestSizeUnitSet(t *testing.T) {
_ = createTestSession(t)
obj := new(dataObject)
obj.SetPropertyValue("x", "20")
obj.SetPropertyValue("size", "10mm")
size := SizeUnit{Auto, 0}
if size.setProperty(obj, "size", new(sessionData), nil) && (size.Type != SizeInMM || size.Value != 10) {
t.Errorf("result: Type = %d, Value = %g", size.Type, size.Value)
}
}
func TestSizeUnitSetValue(t *testing.T) {
_ = createTestSession(t)
type testData struct {
text string
size SizeUnit
}
testValues := []testData{
testData{"auto", SizeUnit{Auto, 0}},
testData{"1.5em", SizeUnit{SizeInEM, 1.5}},
testData{"2ex", SizeUnit{SizeInEX, 2}},
testData{"20px", SizeUnit{SizeInPixel, 20}},
testData{"100%", SizeUnit{SizeInPercent, 100}},
testData{"14pt", SizeUnit{SizeInPt, 14}},
testData{"10pc", SizeUnit{SizeInPc, 10}},
testData{"0.1in", SizeUnit{SizeInInch, 0.1}},
testData{"10mm", SizeUnit{SizeInMM, 10}},
testData{"90.5cm", SizeUnit{SizeInCM, 90.5}},
}
var size SizeUnit
for _, data := range testValues {
if size.SetValue(data.text) && size != data.size {
t.Errorf("set \"%s\" result: Type = %d, Value = %g", data.text, size.Type, size.Value)
}
}
failValues := []string{
"xxx",
"10.10.10px",
"1000",
"5km",
}
for _, text := range failValues {
size.SetValue(text)
}
}
func TestSizeUnitWriteData(t *testing.T) {
_ = createTestSession(t)
type testData struct {
text string
size SizeUnit
}
testValues := []testData{
testData{"auto", SizeUnit{Auto, 0}},
testData{"1.5em", SizeUnit{SizeInEM, 1.5}},
testData{"2ex", SizeUnit{SizeInEX, 2}},
testData{"20px", SizeUnit{SizeInPixel, 20}},
testData{"100%", SizeUnit{SizeInPercent, 100}},
testData{"14pt", SizeUnit{SizeInPt, 14}},
testData{"10pc", SizeUnit{SizeInPc, 10}},
testData{"0.1in", SizeUnit{SizeInInch, 0.1}},
testData{"10mm", SizeUnit{SizeInMM, 10}},
testData{"90.5cm", SizeUnit{SizeInCM, 90.5}},
}
buffer := new(bytes.Buffer)
for _, data := range testValues {
buffer.Reset()
buffer.WriteString(data.size.String())
str := buffer.String()
if str != data.text {
t.Errorf("result: \"%s\", expected: \"%s\"", str, data.text)
}
}
}
*/

290
stackLayout.go Normal file
View File

@ -0,0 +1,290 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
const (
// DefaultAnimation - default animation of StackLayout push
DefaultAnimation = 0
// StartToEndAnimation - start to end animation of StackLayout push
StartToEndAnimation = 1
// EndToStartAnimation - end to start animation of StackLayout push
EndToStartAnimation = 2
// TopDownAnimation - top down animation of StackLayout push
TopDownAnimation = 3
// BottomUpAnimation - bottom up animation of StackLayout push
BottomUpAnimation = 4
)
// StackLayout - list-container of View
type StackLayout interface {
ViewsContainer
Peek() View
MoveToFront(view View) bool
MoveToFrontByID(viewID string) bool
Push(view View, animation int, onPushFinished func())
Pop(animation int, onPopFinished func(View)) bool
}
type stackLayoutData struct {
viewsContainerData
peek uint
pushView, popView View
animationType int
onPushFinished func()
onPopFinished func(View)
}
// NewStackLayout create new StackLayout object and return it
func NewStackLayout(session Session, params Params) StackLayout {
view := new(stackLayoutData)
view.Init(session)
setInitParams(view, params)
return view
}
func newStackLayout(session Session) View {
return NewStackLayout(session, nil)
}
// Init initialize fields of ViewsContainer by default values
func (layout *stackLayoutData) Init(session Session) {
layout.viewsContainerData.Init(session)
layout.tag = "StackLayout"
layout.systemClass = "ruiStackLayout"
}
func (layout *stackLayoutData) OnAnimationFinished(view View, tag string) {
switch tag {
case "ruiPush":
if layout.pushView != nil {
layout.pushView = nil
count := len(layout.views)
if count > 0 {
layout.peek = uint(count - 1)
} else {
layout.peek = 0
}
updateInnerHTML(layout.htmlID(), layout.session)
}
if layout.onPushFinished != nil {
onPushFinished := layout.onPushFinished
layout.onPushFinished = nil
onPushFinished()
}
case "ruiPop":
popView := layout.popView
layout.popView = nil
updateInnerHTML(layout.htmlID(), layout.session)
if layout.onPopFinished != nil {
onPopFinished := layout.onPopFinished
layout.onPopFinished = nil
onPopFinished(popView)
}
}
}
func (layout *stackLayoutData) Peek() View {
if int(layout.peek) < len(layout.views) {
return layout.views[layout.peek]
}
return nil
}
func (layout *stackLayoutData) MoveToFront(view View) bool {
peek := int(layout.peek)
htmlID := view.htmlID()
for i, view2 := range layout.views {
if view2.htmlID() == htmlID {
if i != peek {
if peek < len(layout.views) {
updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden", layout.Session())
}
layout.peek = uint(i)
updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible", layout.Session())
}
return true
}
}
ErrorLog(`MoveToFront() fail. Subview not found."`)
return false
}
func (layout *stackLayoutData) MoveToFrontByID(viewID string) bool {
peek := int(layout.peek)
for i, view := range layout.views {
if view.ID() == viewID {
if i != peek {
if peek < len(layout.views) {
updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden", layout.Session())
}
layout.peek = uint(i)
updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible", layout.Session())
}
return true
}
}
ErrorLogF(`MoveToFront("%s") fail. Subview with "%s" not found."`, viewID, viewID)
return false
}
func (layout *stackLayoutData) Append(view View) {
if view != nil {
layout.peek = uint(len(layout.views))
layout.viewsContainerData.Append(view)
} else {
ErrorLog("StackLayout.Append(nil, ....) is forbidden")
}
}
func (layout *stackLayoutData) Insert(view View, index uint) {
if view != nil {
count := uint(len(layout.views))
if index < count {
layout.peek = index
} else {
layout.peek = count
}
layout.viewsContainerData.Insert(view, index)
} else {
ErrorLog("StackLayout.Insert(nil, ....) is forbidden")
}
}
func (layout *stackLayoutData) RemoveView(index uint) View {
if layout.peek > 0 {
layout.peek--
}
return layout.viewsContainerData.RemoveView(index)
}
func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) {
if view == nil {
ErrorLog("StackLayout.Push(nil, ....) is forbidden")
return
}
layout.pushView = view
layout.animationType = animation
layout.animation["ruiPush"] = Animation{FinishListener: layout}
layout.onPushFinished = onPushFinished
htmlID := layout.htmlID()
session := layout.Session()
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<div id="`)
buffer.WriteString(htmlID)
buffer.WriteString(`push" class="ruiStackPageLayout" ontransitionend="stackTransitionEndEvent(\'`)
buffer.WriteString(htmlID)
buffer.WriteString(`\', \'ruiPush\', event)" style="`)
switch layout.animationType {
case StartToEndAnimation:
buffer.WriteString(fmt.Sprintf("transform: translate(-%gpx, 0px); transition: transform ", layout.frame.Width))
case TopDownAnimation:
buffer.WriteString(fmt.Sprintf("transform: translate(0px, -%gpx); transition: transform ", layout.frame.Height))
case BottomUpAnimation:
buffer.WriteString(fmt.Sprintf("transform: translate(0px, %gpx); transition: transform ", layout.frame.Height))
default:
buffer.WriteString(fmt.Sprintf("transform: translate(%gpx, 0px); transition: transform ", layout.frame.Width))
}
buffer.WriteString(`1s ease;">`)
viewHTML(layout.pushView, buffer)
buffer.WriteString(`</div>`)
appendToInnerHTML(htmlID, buffer.String(), session)
updateCSSProperty(htmlID+"push", "transform", "translate(0px, 0px)", layout.session)
layout.views = append(layout.views, view)
view.setParentID(htmlID)
}
func (layout *stackLayoutData) Pop(animation int, onPopFinished func(View)) bool {
count := uint(len(layout.views))
if count == 0 || layout.peek >= count {
ErrorLog("StackLayout is empty")
return false
}
layout.popView = layout.views[layout.peek]
layout.RemoveView(layout.peek)
layout.animationType = animation
layout.animation["ruiPop"] = Animation{FinishListener: layout}
layout.onPopFinished = onPopFinished
htmlID := layout.htmlID()
session := layout.Session()
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<div id="`)
buffer.WriteString(htmlID)
buffer.WriteString(`pop" class="ruiStackPageLayout" ontransitionend="stackTransitionEndEvent(\'`)
buffer.WriteString(htmlID)
buffer.WriteString(`\', \'ruiPop\', event)" style="transition: transform 1s ease;">`)
viewHTML(layout.popView, buffer)
buffer.WriteString(`</div>`)
appendToInnerHTML(htmlID, buffer.String(), session)
var value string
switch layout.animationType {
case TopDownAnimation:
value = fmt.Sprintf("translate(0px, -%gpx)", layout.frame.Height)
case BottomUpAnimation:
value = fmt.Sprintf("translate(0px, %gpx)", layout.frame.Height)
case StartToEndAnimation:
value = fmt.Sprintf("translate(-%gpx, 0px)", layout.frame.Width)
default:
value = fmt.Sprintf("translate(%gpx, 0px)", layout.frame.Width)
}
updateCSSProperty(htmlID+"pop", "transform", value, layout.session)
return true
}
func (layout *stackLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
count := len(layout.views)
if count > 0 {
htmlID := layout.htmlID()
peek := int(layout.peek)
if peek >= count {
peek = count - 1
}
for i, view := range layout.views {
buffer.WriteString(`<div id="`)
buffer.WriteString(htmlID)
buffer.WriteString(`page`)
buffer.WriteString(strconv.Itoa(i))
buffer.WriteString(`" class="ruiStackPageLayout"`)
if i != peek {
buffer.WriteString(` style="visibility: hidden;"`)
}
buffer.WriteString(`>`)
viewHTML(view, buffer)
buffer.WriteString(`</div>`)
}
}
}

128
strings.go Normal file
View File

@ -0,0 +1,128 @@
package rui
import (
"embed"
"io/ioutil"
"path/filepath"
"strings"
)
var stringResources = map[string]map[string]string{}
func scanEmbedStringsDir(fs *embed.FS, dir string) {
if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files {
name := file.Name()
path := dir + "/" + name
if file.IsDir() {
scanEmbedStringsDir(fs, path)
} else if strings.ToLower(filepath.Ext(name)) == ".rui" {
if data, err := fs.ReadFile(path); err == nil {
loadStringResources(string(data))
} else {
ErrorLog(err.Error())
}
}
}
}
}
func scanStringsDir(path string) {
if files, err := ioutil.ReadDir(path); err == nil {
for _, file := range files {
filename := file.Name()
if filename[0] != '.' {
newPath := path + `/` + filename
if file.IsDir() {
scanStringsDir(newPath)
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
if data, err := ioutil.ReadFile(newPath); err == nil {
loadStringResources(string(data))
} else {
ErrorLog(err.Error())
}
}
}
}
} else {
ErrorLog(err.Error())
}
}
func loadStringResources(text string) {
data := ParseDataText(text)
if data == nil {
return
}
parseStrings := func(obj DataObject, lang string) {
table, ok := stringResources[lang]
if !ok {
table = map[string]string{}
}
for i := 0; i < obj.PropertyCount(); i++ {
if prop := obj.Property(i); prop != nil && prop.Type() == TextNode {
table[prop.Tag()] = prop.Text()
}
}
stringResources[lang] = table
}
tag := data.Tag()
if tag == "strings" {
for i := 0; i < data.PropertyCount(); i++ {
if prop := data.Property(i); prop != nil && prop.Type() == ObjectNode {
parseStrings(prop.Object(), prop.Tag())
}
}
} else if strings.HasPrefix(tag, "strings:") {
if lang := tag[8:]; lang != "" {
parseStrings(data, lang)
}
}
}
// GetString returns the text for the language which is defined by "lang" parameter
func GetString(tag, lang string) (string, bool) {
if table, ok := stringResources[lang]; ok {
if text, ok := table[tag]; ok {
return text, true
}
DebugLogF(`There is no "%s" string resource`, tag)
}
DebugLogF(`There are no "%s" language resources`, lang)
return tag, false
}
func (session *sessionData) GetString(tag string) (string, bool) {
getString := func(tag, lang string) (string, bool) {
if table, ok := stringResources[lang]; ok {
if text, ok := table[tag]; ok {
return text, true
}
DebugLogF(`There is no "%s" string in "%s" resources`, tag, lang)
}
return tag, false
}
if session.language != "" {
if text, ok := getString(tag, session.language); ok {
return text, true
}
}
if session.languages != nil {
for _, lang := range session.languages {
if lang != session.language {
if text, ok := getString(tag, lang); ok {
return text, true
}
}
}
}
return tag, false
}

331
tableAdapter.go Normal file
View File

@ -0,0 +1,331 @@
package rui
type TableAdapter interface {
RowCount() int
ColumnCount() int
Cell(row, column int) interface{}
}
type TableColumnStyle interface {
ColumnStyle(column int) Params
}
type TableRowStyle interface {
RowStyle(row int) Params
}
type TableCellStyle interface {
CellStyle(row, column int) Params
}
type SimpleTableAdapter interface {
TableAdapter
TableCellStyle
}
type simpleTableAdapter struct {
content [][]interface{}
columnCount int
}
type TextTableAdapter interface {
TableAdapter
}
type textTableAdapter struct {
content [][]string
columnCount int
}
type VerticalTableJoin struct {
}
type HorizontalTableJoin struct {
}
func NewSimpleTableAdapter(content [][]interface{}) SimpleTableAdapter {
if content == nil {
return nil
}
adapter := new(simpleTableAdapter)
adapter.content = content
adapter.columnCount = 0
for _, row := range content {
if row != nil {
columnCount := len(row)
if adapter.columnCount < columnCount {
adapter.columnCount = columnCount
}
}
}
return adapter
}
func (adapter *simpleTableAdapter) RowCount() int {
if adapter.content != nil {
return len(adapter.content)
}
return 0
}
func (adapter *simpleTableAdapter) ColumnCount() int {
return adapter.columnCount
}
func (adapter *simpleTableAdapter) Cell(row, column int) interface{} {
if adapter.content != nil && row >= 0 && row < len(adapter.content) &&
adapter.content[row] != nil && column >= 0 && column < len(adapter.content[row]) {
return adapter.content[row][column]
}
return nil
}
func (adapter *simpleTableAdapter) CellStyle(row, column int) Params {
if adapter.content == nil {
return nil
}
getColumnSpan := func() int {
count := 0
for i := column + 1; i < adapter.columnCount; i++ {
next := adapter.Cell(row, i)
switch next.(type) {
case HorizontalTableJoin:
count++
default:
return count
}
}
return count
}
getRowSpan := func() int {
rowCount := len(adapter.content)
count := 0
for i := row + 1; i < rowCount; i++ {
next := adapter.Cell(i, column)
switch next.(type) {
case VerticalTableJoin:
count++
default:
return count
}
}
return count
}
columnSpan := getColumnSpan()
rowSpan := getRowSpan()
var params Params = nil
if rowSpan > 0 {
params = Params{RowSpan: rowSpan + 1}
}
if columnSpan > 0 {
if params == nil {
params = Params{ColumnSpan: columnSpan + 1}
} else {
params[ColumnSpan] = columnSpan
}
}
return params
}
func NewTextTableAdapter(content [][]string) TextTableAdapter {
if content == nil {
return nil
}
adapter := new(textTableAdapter)
adapter.content = content
adapter.columnCount = 0
for _, row := range content {
if row != nil {
columnCount := len(row)
if adapter.columnCount < columnCount {
adapter.columnCount = columnCount
}
}
}
return adapter
}
func (adapter *textTableAdapter) RowCount() int {
if adapter.content != nil {
return len(adapter.content)
}
return 0
}
func (adapter *textTableAdapter) ColumnCount() int {
return adapter.columnCount
}
func (adapter *textTableAdapter) Cell(row, column int) interface{} {
if adapter.content != nil && row >= 0 && row < len(adapter.content) &&
adapter.content[row] != nil && column >= 0 && column < len(adapter.content[row]) {
return adapter.content[row][column]
}
return nil
}
type simpleTableRowStyle struct {
params []Params
}
func (style *simpleTableRowStyle) RowStyle(row int) Params {
if row < len(style.params) {
params := style.params[row]
if len(params) > 0 {
return params
}
}
return nil
}
func (table *tableViewData) setRowStyle(value interface{}) bool {
newSimpleTableRowStyle := func(params []Params) TableRowStyle {
if len(params) == 0 {
return nil
}
result := new(simpleTableRowStyle)
result.params = params
return result
}
switch value := value.(type) {
case TableRowStyle:
table.properties[RowStyle] = value
case []Params:
if style := newSimpleTableRowStyle(value); style != nil {
table.properties[RowStyle] = style
} else {
delete(table.properties, RowStyle)
}
case DataNode:
if value.Type() == ArrayNode {
params := make([]Params, value.ArraySize())
for i, element := range value.ArrayElements() {
params[i] = Params{}
if element.IsObject() {
obj := element.Object()
for k := 0; k < obj.PropertyCount(); k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
params[i][prop.Tag()] = prop.Text()
}
}
} else {
params[i][Style] = element.Value()
}
}
if style := newSimpleTableRowStyle(params); style != nil {
table.properties[RowStyle] = style
} else {
delete(table.properties, RowStyle)
}
} else {
return false
}
default:
return false
}
return true
}
func (table *tableViewData) getRowStyle() TableRowStyle {
for _, tag := range []string{RowStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableRowStyle); ok {
return style
}
}
}
return nil
}
type simpleTableColumnStyle struct {
params []Params
}
func (style *simpleTableColumnStyle) ColumnStyle(row int) Params {
if row < len(style.params) {
params := style.params[row]
if len(params) > 0 {
return params
}
}
return nil
}
func (table *tableViewData) setColumnStyle(value interface{}) bool {
newSimpleTableColumnStyle := func(params []Params) TableColumnStyle {
if len(params) == 0 {
return nil
}
result := new(simpleTableColumnStyle)
result.params = params
return result
}
switch value := value.(type) {
case TableColumnStyle:
table.properties[ColumnStyle] = value
case []Params:
if style := newSimpleTableColumnStyle(value); style != nil {
table.properties[ColumnStyle] = style
} else {
delete(table.properties, ColumnStyle)
}
case DataNode:
if value.Type() == ArrayNode {
params := make([]Params, value.ArraySize())
for i, element := range value.ArrayElements() {
params[i] = Params{}
if element.IsObject() {
obj := element.Object()
for k := 0; k < obj.PropertyCount(); k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
params[i][prop.Tag()] = prop.Text()
}
}
} else {
params[i][Style] = element.Value()
}
}
if style := newSimpleTableColumnStyle(params); style != nil {
table.properties[ColumnStyle] = style
} else {
delete(table.properties, ColumnStyle)
}
} else {
return false
}
default:
return false
}
return true
}
func (table *tableViewData) getColumnStyle() TableColumnStyle {
for _, tag := range []string{ColumnStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableColumnStyle); ok {
return style
}
}
}
return nil
}

842
tableView.go Normal file
View File

@ -0,0 +1,842 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
const (
// TableVerticalAlign is the constant for the "table-vertical-align" property tag.
// The "table-vertical-align" int property sets the vertical alignment of the content inside a table cell.
// Valid values are LeftAlign (0), RightAlign (1), CenterAlign (2), and BaselineAlign (3, 4)
TableVerticalAlign = "table-vertical-align"
// HeadHeight is the constant for the "head-height" property tag.
// The "head-height" int property sets the number of rows in the table header.
// The default value is 0 (no header)
HeadHeight = "head-height"
// HeadStyle is the constant for the "head-style" property tag.
// The "head-style" string property sets the header style name
HeadStyle = "head-style"
// FootHeight is the constant for the "foot-height" property tag.
// The "foot-height" int property sets the number of rows in the table footer.
// The default value is 0 (no footer)
FootHeight = "foot-height"
// FootStyle is the constant for the "foot-style" property tag.
// The "foot-style" string property sets the footer style name
FootStyle = "foot-style"
// RowSpan is the constant for the "row-span" property tag.
// The "row-span" int property sets the number of table row to span.
// Used only when specifying cell parameters in the implementation of TableCellStyle
RowSpan = "row-span"
// ColumnSpan is the constant for the "column-span" property tag.
// The "column-span" int property sets the number of table column to span.
// Used only when specifying cell parameters in the implementation of TableCellStyle
ColumnSpan = "column-span"
// RowStyle is the constant for the "row-style" property tag.
// The "row-style" property sets the adapter which specifies styles of each table row.
// This property can be assigned or by an implementation of TableRowStyle interface, or by an array of Params.
RowStyle = "row-style"
// ColumnStyle is the constant for the "column-style" property tag.
// The "column-style" property sets the adapter which specifies styles of each table column.
// This property can be assigned or by an implementation of TableColumnStyle interface, or by an array of Params.
ColumnStyle = "column-style"
// CellStyle is the constant for the "cell-style" property tag.
// The "cell-style" property sets the adapter which specifies styles of each table cell.
// This property can be assigned only by an implementation of TableCellStyle interface.
CellStyle = "cell-style"
// CellPadding is the constant for the "cell-padding" property tag.
// The "cell-padding" Bounds property sets the padding area on all four sides of a table call at once.
// An element's padding area is the space between its content and its border.
CellPadding = "cell-padding"
// CellPaddingLeft is the constant for the "cell-padding-left" property tag.
// The "cell-padding-left" SizeUnit property sets the width of the padding area to the left of a cell content.
// An element's padding area is the space between its content and its border.
CellPaddingLeft = "cell-padding-left"
// CellPaddingRight is the constant for the "cell-padding-right" property tag.
// The "cell-padding-right" SizeUnit property sets the width of the padding area to the left of a cell content.
// An element's padding area is the space between its content and its border.
CellPaddingRight = "cell-padding-right"
// CellPaddingTop is the constant for the "cell-padding-top" property tag.
// The "cell-padding-top" SizeUnit property sets the height of the padding area to the top of a cell content.
// An element's padding area is the space between its content and its border.
CellPaddingTop = "cell-padding-top"
// CellPaddingBottom is the constant for the "cell-padding-bottom" property tag.
// The "cell-padding-bottom" SizeUnit property sets the height of the padding area to the bottom of a cell content.
CellPaddingBottom = "cell-padding-bottom"
// CellBorder is the constant for the "cell-border" property tag.
// The "cell-border" property sets a table cell's border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
CellBorder = "cell-border"
// CellBorderLeft is the constant for the "cell-border-left" property tag.
// The "cell-border-left" property sets a view's left border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
CellBorderLeft = "cell-border-left"
// CellBorderRight is the constant for the "cell-border-right" property tag.
// The "cell-border-right" property sets a view's right border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
CellBorderRight = "cell-border-right"
// CellBorderTop is the constant for the "cell-border-top" property tag.
// The "cell-border-top" property sets a view's top border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
CellBorderTop = "cell-border-top"
// CellBorderBottom is the constant for the "cell-border-bottom" property tag.
// The "cell-border-bottom" property sets a view's bottom border. It sets the values of a border width, style, and color.
// This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation.
CellBorderBottom = "cell-border-bottom"
// CellBorderStyle is the constant for the "cell-border-style" property tag.
// The "cell-border-style" int property sets the line style for all four sides of a table cell's border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
CellBorderStyle = "cell-border-style"
// CellBorderLeftStyle is the constant for the "cell-border-left-style" property tag.
// The "cell-border-left-style" int property sets the line style of a table cell's left border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
CellBorderLeftStyle = "cell-border-left-style"
// CellBorderRightStyle is the constant for the "cell-border-right-style" property tag.
// The "cell-border-right-style" int property sets the line style of a table cell's right border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
CellBorderRightStyle = "cell-border-right-style"
// CellBorderTopStyle is the constant for the "cell-border-top-style" property tag.
// The "cell-border-top-style" int property sets the line style of a table cell's top border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
CellBorderTopStyle = "cell-border-top-style"
// CellBorderBottomStyle is the constant for the "cell-border-bottom-style" property tag.
// The "cell-border-bottom-style" int property sets the line style of a table cell's bottom border.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
CellBorderBottomStyle = "cell-border-bottom-style"
// CellBorderWidth is the constant for the "cell-border-width" property tag.
// The "cell-border-width" property sets the line width for all four sides of a table cell's border.
CellBorderWidth = "cell-border-width"
// CellBorderLeftWidth is the constant for the "cell-border-left-width" property tag.
// The "cell-border-left-width" SizeUnit property sets the line width of a table cell's left border.
CellBorderLeftWidth = "cell-border-left-width"
// CellBorderRightWidth is the constant for the "cell-border-right-width" property tag.
// The "cell-border-right-width" SizeUnit property sets the line width of a table cell's right border.
CellBorderRightWidth = "cell-border-right-width"
// CellBorderTopWidth is the constant for the "cell-border-top-width" property tag.
// The "cell-border-top-width" SizeUnit property sets the line width of a table cell's top border.
CellBorderTopWidth = "cell-border-top-width"
// CellBorderBottomWidth is the constant for the "cell-border-bottom-width" property tag.
// The "cell-border-bottom-width" SizeUnit property sets the line width of a table cell's bottom border.
CellBorderBottomWidth = "cell-border-bottom-width"
// CellBorderColor is the constant for the "cell-border-color" property tag.
// The "cell-border-color" property sets the line color for all four sides of a table cell's border.
CellBorderColor = "cell-border-color"
// CellBorderLeftColor is the constant for the "cell-border-left-color" property tag.
// The "cell-border-left-color" property sets the line color of a table cell's left border.
CellBorderLeftColor = "cell-border-left-color"
// CellBorderRightColor is the constant for the "cell-border-right-color" property tag.
// The "cell-border-right-color" property sets the line color of a table cell's right border.
CellBorderRightColor = "cell-border-right-color"
// CellBorderTopColor is the constant for the "cell-border-top-color" property tag.
// The "cell-border-top-color" property sets the line color of a table cell's top border.
CellBorderTopColor = "cell-border-top-color"
// CellBorderBottomColor is the constant for the "cell-border-bottom-color" property tag.
// The "cell-border-bottom-color" property sets the line color of a table cell's bottom border.
CellBorderBottomColor = "cell-border-bottom-color"
)
// TableView - text View
type TableView interface {
View
ReloadTableData()
}
type tableViewData struct {
viewData
}
type tableCellView struct {
viewData
}
// NewTableView create new TableView object and return it
func NewTableView(session Session, params Params) TableView {
view := new(tableViewData)
view.Init(session)
setInitParams(view, params)
return view
}
func newTableView(session Session) View {
return NewTableView(session, nil)
}
// Init initialize fields of TableView by default values
func (table *tableViewData) Init(session Session) {
table.viewData.Init(session)
table.tag = "TableView"
}
func (table *tableViewData) Get(tag string) interface{} {
return table.get(strings.ToLower(tag))
}
func (table *tableViewData) Remove(tag string) {
table.remove(strings.ToLower(tag))
}
func (table *tableViewData) remove(tag string) {
switch tag {
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft,
"top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding":
table.removeBoundsSide(CellPadding, tag)
case Gap, CellBorder, CellPadding, RowStyle, ColumnStyle, CellStyle,
HeadHeight, HeadStyle, FootHeight, FootStyle:
delete(table.properties, tag)
default:
table.viewData.remove(tag)
return
}
table.propertyChanged(tag)
}
func (table *tableViewData) Set(tag string, value interface{}) bool {
return table.set(strings.ToLower(tag), value)
}
func (table *tableViewData) set(tag string, value interface{}) bool {
if value == nil {
table.remove(tag)
return true
}
switch tag {
case Content:
switch val := value.(type) {
case TableAdapter:
table.properties[Content] = value
case [][]interface{}:
table.properties[Content] = NewSimpleTableAdapter(val)
case [][]string:
table.properties[Content] = NewTextTableAdapter(val)
default:
notCompatibleType(tag, value)
return false
}
case CellStyle:
if style, ok := value.(TableCellStyle); ok {
table.properties[tag] = style
} else {
notCompatibleType(tag, value)
return false
}
case RowStyle:
if !table.setRowStyle(value) {
notCompatibleType(tag, value)
return false
}
case ColumnStyle:
if !table.setColumnStyle(value) {
notCompatibleType(tag, value)
return false
}
case HeadHeight, FootHeight:
switch value := value.(type) {
case string:
if isConstantName(value) {
table.properties[tag] = value
} else if n, err := strconv.Atoi(value); err == nil {
table.properties[tag] = n
} else {
ErrorLog(err.Error())
notCompatibleType(tag, value)
return false
}
default:
if n, ok := isInt(value); ok {
table.properties[tag] = n
}
}
case HeadStyle, FootStyle:
switch value := value.(type) {
case string:
table.properties[tag] = value
case Params:
if len(value) > 0 {
table.properties[tag] = value
} else {
delete(table.properties, tag)
}
case DataNode:
switch value.Type() {
case ObjectNode:
obj := value.Object()
params := Params{}
for k := 0; k < obj.PropertyCount(); k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
params[prop.Tag()] = prop.Text()
}
}
if len(params) > 0 {
table.properties[tag] = params
} else {
delete(table.properties, tag)
}
case TextNode:
table.properties[tag] = value.Text()
default:
notCompatibleType(tag, value)
return false
}
default:
notCompatibleType(tag, value)
return false
}
case CellPadding:
if !table.setBounds(tag, value) {
return false
}
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft,
"top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding":
if !table.setBoundsSide(CellPadding, tag, value) {
return false
}
case Gap:
if !table.setSizeProperty(Gap, value) {
return false
}
case CellBorder, CellBorderStyle, CellBorderColor, CellBorderWidth,
CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth,
CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth,
CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth,
CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth:
if !table.viewData.set(tag, value) {
return false
}
default:
return table.viewData.set(tag, value)
}
table.propertyChanged(tag)
return true
}
func (table *tableViewData) propertyChanged(tag string) {
switch tag {
case Content, RowStyle, ColumnStyle, CellStyle, CellPadding, CellBorder,
HeadHeight, HeadStyle, FootHeight, FootStyle,
CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft,
"top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding":
table.ReloadTableData()
case Gap:
htmlID := table.htmlID()
session := table.Session()
gap, ok := sizeProperty(table, Gap, session)
if !ok || gap.Type == Auto || gap.Value <= 0 {
updateCSSProperty(htmlID, "border-spacing", "0", session)
updateCSSProperty(htmlID, "border-collapse", "collapse", session)
} else {
updateCSSProperty(htmlID, "border-spacing", gap.cssString("0"), session)
updateCSSProperty(htmlID, "border-collapse", "separate", session)
}
}
}
func (table *tableViewData) htmlTag() string {
return "table"
}
func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
content := table.getRaw(Content)
if content == nil {
return
}
adapter, ok := content.(TableAdapter)
if !ok {
return
}
rowCount := adapter.RowCount()
columnCount := adapter.ColumnCount()
if rowCount == 0 || columnCount == 0 {
return
}
rowStyle := table.getRowStyle()
var cellStyle1 TableCellStyle = nil
if style, ok := content.(TableCellStyle); ok {
cellStyle1 = style
}
var cellStyle2 TableCellStyle = nil
if value := table.getRaw(CellStyle); value != nil {
if style, ok := value.(TableCellStyle); ok {
cellStyle2 = style
}
}
session := table.Session()
if !session.ignoreViewUpdates() {
session.setIgnoreViewUpdates(true)
defer session.setIgnoreViewUpdates(false)
}
var cssBuilder viewCSSBuilder
cssBuilder.buffer = allocStringBuilder()
defer freeStringBuilder(cssBuilder.buffer)
var view tableCellView
view.Init(session)
ignorCells := []struct{ row, column int }{}
tableCSS := func(startRow, endRow int, cellTag string, cellBorder BorderProperty, cellPadding BoundsProperty) {
for row := startRow; row < endRow; row++ {
cssBuilder.buffer.Reset()
if rowStyle != nil {
if styles := rowStyle.RowStyle(row); styles != nil {
view.Clear()
for tag, value := range styles {
view.Set(tag, value)
}
view.cssStyle(&view, &cssBuilder)
}
}
if cssBuilder.buffer.Len() > 0 {
buffer.WriteString(`<tr style="`)
buffer.WriteString(cssBuilder.buffer.String())
buffer.WriteString(`">`)
} else {
buffer.WriteString("<tr>")
}
for column := 0; column < columnCount; column++ {
ignore := false
for _, cell := range ignorCells {
if cell.row == row && cell.column == column {
ignore = true
break
}
}
if !ignore {
rowSpan := 0
columnSpan := 0
cssBuilder.buffer.Reset()
view.Clear()
if cellBorder != nil {
view.set(Border, cellBorder)
}
if cellPadding != nil {
view.set(Padding, cellPadding)
}
appendFrom := func(cellStyle TableCellStyle) {
if cellStyle != nil {
if styles := cellStyle.CellStyle(row, column); styles != nil {
for tag, value := range styles {
valueToInt := func() int {
switch value := value.(type) {
case int:
return value
case string:
if value, ok = session.resolveConstants(value); ok {
if n, err := strconv.Atoi(value); err == nil {
return n
}
}
}
return 0
}
switch tag = strings.ToLower(tag); tag {
case RowSpan:
rowSpan = valueToInt()
case ColumnSpan:
columnSpan = valueToInt()
default:
view.set(tag, value)
}
}
}
}
}
appendFrom(cellStyle1)
appendFrom(cellStyle2)
if len(view.properties) > 0 {
view.cssStyle(&view, &cssBuilder)
}
buffer.WriteRune('<')
buffer.WriteString(cellTag)
if columnSpan > 1 {
buffer.WriteString(` colspan="`)
buffer.WriteString(strconv.Itoa(columnSpan))
buffer.WriteRune('"')
for c := column + 1; c < column+columnSpan; c++ {
ignorCells = append(ignorCells, struct {
row int
column int
}{row: row, column: c})
}
}
if rowSpan > 1 {
buffer.WriteString(` rowspan="`)
buffer.WriteString(strconv.Itoa(rowSpan))
buffer.WriteRune('"')
if columnSpan < 1 {
columnSpan = 1
}
for r := row + 1; r < row+rowSpan; r++ {
for c := column; c < column+columnSpan; c++ {
ignorCells = append(ignorCells, struct {
row int
column int
}{row: r, column: c})
}
}
}
if cssBuilder.buffer.Len() > 0 {
buffer.WriteString(` style="`)
buffer.WriteString(cssBuilder.buffer.String())
buffer.WriteRune('"')
}
buffer.WriteRune('>')
switch value := adapter.Cell(row, column).(type) {
case string:
buffer.WriteString(value)
case View:
viewHTML(value, buffer)
case Color:
buffer.WriteString(`<div style="display: inline; height: 1em; background-color: `)
buffer.WriteString(value.cssString())
buffer.WriteString(`">&nbsp;&nbsp;&nbsp;&nbsp;</div> `)
buffer.WriteString(value.String())
case fmt.Stringer:
buffer.WriteString(value.String())
case rune:
buffer.WriteRune(value)
case float32:
buffer.WriteString(fmt.Sprintf("%g", float64(value)))
case float64:
buffer.WriteString(fmt.Sprintf("%g", value))
case bool:
if value {
buffer.WriteString(session.checkboxOnImage())
} else {
buffer.WriteString(session.checkboxOffImage())
}
default:
if n, ok := isInt(value); ok {
buffer.WriteString(fmt.Sprintf("%d", n))
} else {
buffer.WriteString("<Unsupported value>")
}
}
buffer.WriteString(`</`)
buffer.WriteString(cellTag)
buffer.WriteRune('>')
}
}
buffer.WriteString("</tr>")
}
}
if columnStyle := table.getColumnStyle(); columnStyle != nil {
buffer.WriteString("<colgroup>")
for column := 0; column < columnCount; column++ {
cssBuilder.buffer.Reset()
if styles := columnStyle.ColumnStyle(column); styles != nil {
view.Clear()
for tag, value := range styles {
view.Set(tag, value)
}
view.cssStyle(&view, &cssBuilder)
}
if cssBuilder.buffer.Len() > 0 {
buffer.WriteString(`<col style="`)
buffer.WriteString(cssBuilder.buffer.String())
buffer.WriteString(`">`)
} else {
buffer.WriteString("<col>")
}
}
buffer.WriteString("</colgroup>")
}
headHeight, _ := intProperty(table, HeadHeight, table.Session(), 0)
footHeight, _ := intProperty(table, FootHeight, table.Session(), 0)
cellBorder := table.getCellBorder()
cellPadding := table.boundsProperty(CellPadding)
if cellPadding == nil {
if style, ok := stringProperty(table, Style, table.Session()); ok {
if style, ok := table.Session().resolveConstants(style); ok {
cellPadding = table.cellPaddingFromStyle(style)
}
}
}
headFootStart := func(htmlTag, styleTag string) (BorderProperty, BoundsProperty) {
buffer.WriteRune('<')
buffer.WriteString(htmlTag)
if value := table.getRaw(styleTag); value != nil {
switch value := value.(type) {
case string:
if style, ok := session.resolveConstants(value); ok {
buffer.WriteString(` class="`)
buffer.WriteString(style)
buffer.WriteString(`">`)
return table.cellBorderFromStyle(style), table.cellPaddingFromStyle(style)
}
case Params:
cssBuilder.buffer.Reset()
view.Clear()
for tag, val := range value {
view.Set(tag, val)
}
var border BorderProperty = nil
if value := view.Get(CellBorder); value != nil {
border = value.(BorderProperty)
}
var padding BoundsProperty = nil
if value := view.Get(CellPadding); value != nil {
switch value := value.(type) {
case SizeUnit:
padding = NewBoundsProperty(Params{
Top: value,
Right: value,
Bottom: value,
Left: value,
})
case BoundsProperty:
padding = value
}
}
view.cssStyle(&view, &cssBuilder)
if cssBuilder.buffer.Len() > 0 {
buffer.WriteString(` style="`)
buffer.WriteString(cssBuilder.buffer.String())
buffer.WriteString(`"`)
}
buffer.WriteRune('>')
return border, padding
}
}
buffer.WriteRune('>')
return nil, nil
}
if headHeight > 0 {
headCellBorder := cellBorder
headCellPadding := cellPadding
if headHeight > rowCount {
headHeight = rowCount
}
border, padding := headFootStart("thead", HeadStyle)
if border != nil {
headCellBorder = border
}
if padding != nil {
headCellPadding = padding
}
tableCSS(0, headHeight, "th", headCellBorder, headCellPadding)
buffer.WriteString("</thead>")
}
if footHeight > rowCount-headHeight {
footHeight = rowCount - headHeight
}
if rowCount > footHeight+headHeight {
buffer.WriteString("<tbody>")
tableCSS(headHeight, rowCount-footHeight, "td", cellBorder, cellPadding)
buffer.WriteString("</tbody>")
}
if footHeight > 0 {
footCellBorder := cellBorder
footCellPadding := cellPadding
border, padding := headFootStart("tfoot", FootStyle)
if border != nil {
footCellBorder = border
}
if padding != nil {
footCellPadding = padding
}
tableCSS(rowCount-footHeight, rowCount, "td", footCellBorder, footCellPadding)
buffer.WriteString("</tfoot>")
}
}
func (table *tableViewData) cellPaddingFromStyle(style string) BoundsProperty {
session := table.Session()
var result BoundsProperty = nil
if node := session.stylePropertyNode(style, CellPadding); node != nil && node.Type() == ObjectNode {
for _, tag := range []string{Left, Right, Top, Bottom} {
if node := node.Object().PropertyWithTag(tag); node != nil && node.Type() == TextNode {
if result == nil {
result = NewBoundsProperty(nil)
}
result.Set(tag, node.Text())
}
}
}
for _, tag := range []string{CellPaddingLeft, CellPaddingRight, CellPaddingTop, CellPaddingBottom} {
if value, ok := session.styleProperty(style, CellPadding); ok {
if result == nil {
result = NewBoundsProperty(nil)
}
result.Set(tag, value)
}
}
return result
}
func (table *tableViewData) cellBorderFromStyle(style string) BorderProperty {
border := new(borderProperty)
border.properties = map[string]interface{}{}
session := table.Session()
if node := session.stylePropertyNode(style, CellBorder); node != nil && node.Type() == ObjectNode {
border.setBorderObject(node.Object())
}
for _, tag := range []string{
CellBorderLeft,
CellBorderRight,
CellBorderTop,
CellBorderBottom,
CellBorderStyle,
CellBorderLeftStyle,
CellBorderRightStyle,
CellBorderTopStyle,
CellBorderBottomStyle,
CellBorderWidth,
CellBorderLeftWidth,
CellBorderRightWidth,
CellBorderTopWidth,
CellBorderBottomWidth,
CellBorderColor,
CellBorderLeftColor,
CellBorderRightColor,
CellBorderTopColor,
CellBorderBottomColor,
} {
if value, ok := session.styleProperty(style, tag); ok {
border.Set(tag, value)
}
}
if len(border.properties) == 0 {
return nil
}
return border
}
func (table *tableViewData) getCellBorder() BorderProperty {
if value := table.getRaw(CellBorder); value != nil {
if border, ok := value.(BorderProperty); ok {
return border
}
}
if style, ok := stringProperty(table, Style, table.Session()); ok {
if style, ok := table.Session().resolveConstants(style); ok {
return table.cellBorderFromStyle(style)
}
}
return nil
}
func (table *tableViewData) cssStyle(self View, builder cssBuilder) {
table.viewData.cssViewStyle(builder, table.Session(), self)
gap, ok := sizeProperty(table, Gap, table.Session())
if !ok || gap.Type == Auto || gap.Value <= 0 {
builder.add("border-spacing", "0")
builder.add("border-collapse", "collapse")
} else {
builder.add("border-spacing", gap.cssString("0"))
builder.add("border-collapse", "separate")
}
}
func (table *tableViewData) ReloadTableData() {
updateInnerHTML(table.htmlID(), table.Session())
}
func (cell *tableCellView) Set(tag string, value interface{}) bool {
return cell.set(strings.ToLower(tag), value)
}
func (cell *tableCellView) set(tag string, value interface{}) bool {
switch tag {
case VerticalAlign:
tag = TableVerticalAlign
}
return cell.viewData.set(tag, value)
}
func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
session := cell.Session()
cell.viewData.cssViewStyle(builder, session, self)
if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok {
builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value])
}
}

490
tabsLayout.go Normal file
View File

@ -0,0 +1,490 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
const (
// HiddenTabs - tabs of TabsLayout are hidden
HiddenTabs = 0
// TopTabs - tabs of TabsLayout are on the top
TopTabs = 1
// BottomTabs - tabs of TabsLayout are on the bottom
BottomTabs = 2
// LeftTabs - tabs of TabsLayout are on the left
LeftTabs = 3
// RightTabs - tabs of TabsLayout are on the right
RightTabs = 4
// LeftListTabs - tabs of TabsLayout are on the left
LeftListTabs = 5
// RightListTabs - tabs of TabsLayout are on the right
RightListTabs = 6
)
// TabsLayoutCurrentChangedListener - listener of the current tab changing
type TabsLayoutCurrentChangedListener interface {
OnTabsLayoutCurrentChanged(tabsLayout TabsLayout, newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)
}
type tabsLayoutCurrentChangedListenerFunc struct {
listenerFunc func(tabsLayout TabsLayout, newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)
}
func (listener *tabsLayoutCurrentChangedListenerFunc) OnTabsLayoutCurrentChanged(tabsLayout TabsLayout,
newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View) {
if listener.listenerFunc != nil {
listener.listenerFunc(tabsLayout, newCurrent, newCurrentView, oldCurrent, oldCurrentView)
}
}
// TabsLayout - multi-tab container of View
type TabsLayout interface {
ViewsContainer
/*
// Current return the index of active tab
currentItem() int
// SetCurrent set the index of active tab
SetCurrent(current int)
// TabsLocation return the location of tabs. It returns one of the following values: HiddenTabs (0),
// TopTabs (1), BottomTabs (2), LeftTabs (3), RightTabs (4), LeftListTabs (5), RightListTabs (6)
tabsLocation() int
// TabsLocation set the location of tabs. Valid values: HiddenTabs (0), TopTabs (1),
// BottomTabs (2), LeftTabs (3), RightTabs (4), LeftListTabs (5), RightListTabs (6)
SetTabsLocation(location int)
// TabStyle() return styles of tab in the passive and the active state
TabStyle() (string, string)
SetTabStyle(tabStyle string, activeTabStyle string)
*/
// SetCurrentTabChangedListener add the listener of the current tab changing
SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener)
// SetCurrentTabChangedListener add the listener function of the current tab changing
SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout,
newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View))
}
type tabsLayoutData struct {
viewsContainerData
//currentTab, tabsLocation int
//tabStyle, activeTabStyle string
tabListener TabsLayoutCurrentChangedListener
}
// NewTabsLayout create new TabsLayout object and return it
func NewTabsLayout(session Session) TabsLayout {
view := new(tabsLayoutData)
view.Init(session)
return view
}
func newTabsLayout(session Session) View {
return NewTabsLayout(session)
}
// Init initialize fields of ViewsContainer by default values
func (tabsLayout *tabsLayoutData) Init(session Session) {
tabsLayout.viewsContainerData.Init(session)
tabsLayout.tag = "TabsLayout"
tabsLayout.systemClass = "ruiTabsLayout"
tabsLayout.tabListener = nil
}
func (tabsLayout *tabsLayoutData) currentItem() int {
result, _ := intProperty(tabsLayout, Current, tabsLayout.session, 0)
return result
}
func (tabsLayout *tabsLayoutData) Set(tag string, value interface{}) bool {
switch tag {
case Current:
oldCurrent := tabsLayout.currentItem()
if !tabsLayout.setIntProperty(Current, value) {
return false
}
if !tabsLayout.session.ignoreViewUpdates() {
current := tabsLayout.currentItem()
if oldCurrent != current {
tabsLayout.session.runScript(fmt.Sprintf("activateTab(%v, %d);", tabsLayout.htmlID(), current))
if tabsLayout.tabListener != nil {
oldView := tabsLayout.views[oldCurrent]
view := tabsLayout.views[current]
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, current, view, oldCurrent, oldView)
}
}
}
case Tabs:
if !tabsLayout.setEnumProperty(Tabs, value, enumProperties[Tabs].values) {
return false
}
if !tabsLayout.session.ignoreViewUpdates() {
htmlID := tabsLayout.htmlID()
updateCSSStyle(htmlID, tabsLayout.session)
updateInnerHTML(htmlID, tabsLayout.session)
}
case TabStyle, CurrentTabStyle:
if value == nil {
delete(tabsLayout.properties, tag)
} else if text, ok := value.(string); ok {
if text == "" {
delete(tabsLayout.properties, tag)
} else {
tabsLayout.properties[tag] = text
}
} else {
notCompatibleType(tag, value)
return false
}
if !tabsLayout.session.ignoreViewUpdates() {
htmlID := tabsLayout.htmlID()
updateProperty(htmlID, "data-tabStyle", tabsLayout.inactiveTabStyle(), tabsLayout.session)
updateProperty(htmlID, "data-activeTabStyle", tabsLayout.activeTabStyle(), tabsLayout.session)
updateInnerHTML(htmlID, tabsLayout.session)
}
default:
return tabsLayout.viewsContainerData.Set(tag, value)
}
return true
}
func (tabsLayout *tabsLayoutData) tabsLocation() int {
tabs, _ := enumProperty(tabsLayout, Tabs, tabsLayout.session, 0)
return tabs
}
func (tabsLayout *tabsLayoutData) inactiveTabStyle() string {
if style, ok := stringProperty(tabsLayout, TabStyle, tabsLayout.session); ok {
return style
}
switch tabsLayout.tabsLocation() {
case LeftTabs, RightTabs:
return "ruiInactiveVerticalTab"
}
return "ruiInactiveTab"
}
func (tabsLayout *tabsLayoutData) activeTabStyle() string {
if style, ok := stringProperty(tabsLayout, CurrentTabStyle, tabsLayout.session); ok {
return style
}
switch tabsLayout.tabsLocation() {
case LeftTabs, RightTabs:
return "ruiActiveVerticalTab"
}
return "ruiActiveTab"
}
func (tabsLayout *tabsLayoutData) TabStyle() (string, string) {
return tabsLayout.inactiveTabStyle(), tabsLayout.activeTabStyle()
}
func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener) {
tabsLayout.tabListener = listener
}
/*
// SetCurrentTabChangedListener add the listener of the current tab changing
func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener) {
tabsLayout.tabListener = listener
}
// SetCurrentTabChangedListener add the listener function of the current tab changing
func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout,
newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)) {
}
*/
func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout,
newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)) {
listener := new(tabsLayoutCurrentChangedListenerFunc)
listener.listenerFunc = listenerFunc
tabsLayout.SetCurrentTabChangedListener(listener)
}
// Append appends view to the end of view list
func (tabsLayout *tabsLayoutData) Append(view View) {
if tabsLayout.views == nil {
tabsLayout.views = []View{}
}
tabsLayout.viewsContainerData.Append(view)
if len(tabsLayout.views) == 1 {
tabsLayout.setIntProperty(Current, 0)
if tabsLayout.tabListener != nil {
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, 0, tabsLayout.views[0], -1, nil)
}
}
updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session)
}
// Insert inserts view to the "index" position in view list
func (tabsLayout *tabsLayoutData) Insert(view View, index uint) {
if tabsLayout.views == nil {
tabsLayout.views = []View{}
}
tabsLayout.viewsContainerData.Insert(view, index)
current := tabsLayout.currentItem()
if current >= int(index) {
tabsLayout.Set(Current, current+1)
}
}
// Remove removes view from list and return it
func (tabsLayout *tabsLayoutData) RemoveView(index uint) View {
if tabsLayout.views == nil {
tabsLayout.views = []View{}
return nil
}
i := int(index)
count := len(tabsLayout.views)
if i >= count {
return nil
}
if count == 1 {
view := tabsLayout.views[0]
tabsLayout.views = []View{}
if tabsLayout.tabListener != nil {
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, 0, nil, 0, view)
}
return view
}
current := tabsLayout.currentItem()
removeCurrent := (i == current)
if i < current || (removeCurrent && i == count-1) {
tabsLayout.properties[Current] = current - 1
if tabsLayout.tabListener != nil {
currentView := tabsLayout.views[current-1]
oldCurrentView := tabsLayout.views[current]
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, current-1, currentView, current, oldCurrentView)
}
}
return tabsLayout.viewsContainerData.RemoveView(index)
}
func (tabsLayout *tabsLayoutData) htmlProperties(self View, buffer *strings.Builder) {
tabsLayout.viewsContainerData.htmlProperties(self, buffer)
buffer.WriteString(` data-inactiveTabStyle="`)
buffer.WriteString(tabsLayout.inactiveTabStyle())
buffer.WriteString(`" data-activeTabStyle="`)
buffer.WriteString(tabsLayout.activeTabStyle())
buffer.WriteString(`" data-current="`)
buffer.WriteString(tabsLayout.htmlID())
buffer.WriteRune('-')
buffer.WriteString(strconv.Itoa(tabsLayout.currentItem()))
buffer.WriteRune('"')
}
func (tabsLayout *tabsLayoutData) cssStyle(self View, builder cssBuilder) {
tabsLayout.viewsContainerData.cssStyle(self, builder)
switch tabsLayout.tabsLocation() {
case TopTabs:
builder.add(`grid-template-rows`, `auto 1fr`)
case BottomTabs:
builder.add(`grid-template-rows`, `1fr auto`)
case LeftTabs, LeftListTabs:
builder.add(`grid-template-columns`, `auto 1fr`)
case RightTabs, RightListTabs:
builder.add(`grid-template-columns`, `1fr auto`)
}
}
func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
if tabsLayout.views == nil {
return
}
//viewCount := len(tabsLayout.views)
current := tabsLayout.currentItem()
location := tabsLayout.tabsLocation()
tabsLayoutID := tabsLayout.htmlID()
if location != HiddenTabs {
tabsHeight, _ := sizeConstant(tabsLayout.session, "ruiTabHeight")
tabsSpace, _ := sizeConstant(tabsLayout.session, "ruiTabSpace")
rowLayout := false
buffer.WriteString(`<div style="display: flex;`)
switch location {
case LeftTabs, LeftListTabs, TopTabs:
buffer.WriteString(` grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;`)
case RightTabs, RightListTabs:
buffer.WriteString(` grid-row-start: 1; grid-row-end: 2; grid-column-start: 2; grid-column-end: 3;`)
case BottomTabs:
buffer.WriteString(` grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;`)
}
buffer.WriteString(` flex-flow: `)
switch location {
case LeftTabs, LeftListTabs, RightTabs, RightListTabs:
buffer.WriteString(`column nowrap; justify-content: flex-start; align-items: stretch;`)
default:
buffer.WriteString(`row nowrap; justify-content: flex-start; align-items: stretch;`)
if tabsHeight.Type != Auto {
buffer.WriteString(` height: `)
buffer.WriteString(tabsHeight.cssString(""))
buffer.WriteByte(';')
}
rowLayout = true
}
var tabsPadding Bounds
if value, ok := tabsLayout.session.Constant("ruiTabPadding"); ok {
if tabsPadding.parse(value, tabsLayout.session) {
if !tabsPadding.allFieldsAuto() {
buffer.WriteByte(' ')
buffer.WriteString(Padding)
buffer.WriteString(`: `)
tabsPadding.writeCSSString(buffer, "0")
buffer.WriteByte(';')
}
}
}
if tabsBackground, ok := tabsLayout.session.Color("tabsBackgroundColor"); ok {
buffer.WriteString(` background-color: `)
buffer.WriteString(tabsBackground.cssString())
buffer.WriteByte(';')
}
buffer.WriteString(`">`)
inactiveStyle := tabsLayout.inactiveTabStyle()
activeStyle := tabsLayout.activeTabStyle()
notTranslate := GetNotTranslate(tabsLayout, "")
last := len(tabsLayout.views) - 1
for n, view := range tabsLayout.views {
title, _ := stringProperty(view, "title", tabsLayout.session)
if !notTranslate {
title, _ = tabsLayout.Session().GetString(title)
}
buffer.WriteString(`<div id="`)
buffer.WriteString(tabsLayoutID)
buffer.WriteByte('-')
buffer.WriteString(strconv.Itoa(n))
buffer.WriteString(`" class="`)
if n == current {
buffer.WriteString(activeStyle)
} else {
buffer.WriteString(inactiveStyle)
}
buffer.WriteString(`" tabindex="0" onclick="tabClickEvent(\'`)
buffer.WriteString(tabsLayoutID)
buffer.WriteString(`\', `)
buffer.WriteString(strconv.Itoa(n))
buffer.WriteString(`, event)`)
buffer.WriteString(`" onclick="tabKeyClickEvent(\'`)
buffer.WriteString(tabsLayoutID)
buffer.WriteString(`\', `)
buffer.WriteString(strconv.Itoa(n))
buffer.WriteString(`, event)" style="display: flex; flex-flow: row nowrap; justify-content: center; align-items: center; `)
if n != last && tabsSpace.Type != Auto && tabsSpace.Value > 0 {
if rowLayout {
buffer.WriteString(` margin-right: `)
buffer.WriteString(tabsSpace.cssString(""))
} else {
buffer.WriteString(` margin-bottom: `)
buffer.WriteString(tabsSpace.cssString(""))
}
buffer.WriteByte(';')
}
switch location {
case LeftListTabs, RightListTabs:
if tabsHeight.Type != Auto {
buffer.WriteString(` height: `)
buffer.WriteString(tabsHeight.cssString(""))
buffer.WriteByte(';')
}
}
buffer.WriteString(`" data-container="`)
buffer.WriteString(tabsLayoutID)
buffer.WriteString(`" data-view="`)
//buffer.WriteString(view.htmlID())
buffer.WriteString(tabsLayoutID)
buffer.WriteString(`-page`)
buffer.WriteString(strconv.Itoa(n))
buffer.WriteString(`"><div`)
switch location {
case LeftTabs:
buffer.WriteString(` style="writing-mode: vertical-lr; transform: rotate(180deg)">`)
case RightTabs:
buffer.WriteString(` style="writing-mode: vertical-lr;">`)
default:
buffer.WriteByte('>')
}
buffer.WriteString(title)
buffer.WriteString(`</div></div>`)
}
buffer.WriteString(`</div>`)
}
for n, view := range tabsLayout.views {
buffer.WriteString(`<div id="`)
buffer.WriteString(tabsLayoutID)
buffer.WriteString(`-page`)
buffer.WriteString(strconv.Itoa(n))
switch location {
case LeftTabs, LeftListTabs:
buffer.WriteString(`" style="position: relative; grid-row-start: 1; grid-row-end: 2; grid-column-start: 2; grid-column-end: 3;`)
case TopTabs:
buffer.WriteString(`" style="position: relative; grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;`)
default:
buffer.WriteString(`" style="position: relative; grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;`)
}
if current != n {
buffer.WriteString(` display: none;`)
}
buffer.WriteString(`">`)
view.addToCSSStyle(map[string]string{`position`: `absolute`, `left`: `0`, `right`: `0`, `top`: `0`, `bottom`: `0`})
viewHTML(tabsLayout.views[n], buffer)
buffer.WriteString(`</div>`)
}
}
func (tabsLayout *tabsLayoutData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "tabClick":
if numberText, ok := data.PropertyValue("number"); ok {
if number, err := strconv.Atoi(numberText); err == nil {
current := tabsLayout.currentItem()
if current != number {
tabsLayout.properties[Current] = number
if tabsLayout.tabListener != nil {
oldView := tabsLayout.views[current]
view := tabsLayout.views[number]
tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, number, view, current, oldView)
}
}
}
}
return true
}
return tabsLayout.viewsContainerData.handleCommand(self, command, data)
}

142
textView.go Normal file
View File

@ -0,0 +1,142 @@
package rui
import (
"fmt"
"strings"
)
// TextView - text View
type TextView interface {
View
}
type textViewData struct {
viewData
// TODO textShadow
}
// NewTextView create new TextView object and return it
func NewTextView(session Session, params Params) TextView {
view := new(textViewData)
view.Init(session)
setInitParams(view, params)
return view
}
func newTextView(session Session) View {
return NewTextView(session, nil)
}
// Init initialize fields of TextView by default values
func (textView *textViewData) Init(session Session) {
textView.viewData.Init(session)
textView.tag = "TextView"
}
func (textView *textViewData) Get(tag string) interface{} {
return textView.get(strings.ToLower(tag))
}
func (textView *textViewData) Remove(tag string) {
textView.remove(strings.ToLower(tag))
}
func (textView *textViewData) remove(tag string) {
textView.viewData.remove(tag)
switch tag {
case Text:
updateInnerHTML(textView.htmlID(), textView.session)
case TextOverflow:
textView.textOverflowUpdated()
}
}
func (textView *textViewData) Set(tag string, value interface{}) bool {
return textView.set(strings.ToLower(tag), value)
}
func (textView *textViewData) set(tag string, value interface{}) bool {
switch tag {
case Text:
switch value := value.(type) {
case string:
textView.properties[Text] = value
case fmt.Stringer:
textView.properties[Text] = value.String()
case float32:
textView.properties[Text] = fmt.Sprintf("%g", float64(value))
case float64:
textView.properties[Text] = fmt.Sprintf("%g", value)
case []rune:
textView.properties[Text] = string(value)
case bool:
if value {
textView.properties[Text] = "true"
} else {
textView.properties[Text] = "false"
}
default:
if n, ok := isInt(value); ok {
textView.properties[Text] = fmt.Sprintf("%d", n)
} else {
notCompatibleType(tag, value)
return false
}
}
updateInnerHTML(textView.htmlID(), textView.session)
return true
case TextOverflow:
if textView.viewData.set(tag, value) {
textView.textOverflowUpdated()
}
}
return textView.viewData.set(tag, value)
}
func (textView *textViewData) textOverflowUpdated() {
session := textView.Session()
if n, ok := enumProperty(textView, TextOverflow, textView.session, 0); ok {
values := enumProperties[TextOverflow].cssValues
if n >= 0 && n < len(values) {
updateCSSProperty(textView.htmlID(), TextOverflow, values[n], session)
return
}
}
updateCSSProperty(textView.htmlID(), TextOverflow, "", session)
}
func (textView *textViewData) htmlSubviews(self View, buffer *strings.Builder) {
if value, ok := stringProperty(textView, Text, textView.Session()); ok {
if !GetNotTranslate(textView, "") {
value, _ = textView.session.GetString(value)
}
text := strings.ReplaceAll(value, `"`, `\"`)
text = strings.ReplaceAll(text, "\n", `\n`)
text = strings.ReplaceAll(text, "\r", `\r`)
buffer.WriteString(strings.ReplaceAll(text, `'`, `\'`))
}
}
// GetTextOverflow returns a value of the "text-overflow" property:
// TextOverflowClip (0) or TextOverflowEllipsis (1).
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTextOverflow(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return SingleLineText
}
t, _ := enumStyledProperty(view, TextOverflow, SingleLineText)
return t
}

329
theme.go Normal file
View File

@ -0,0 +1,329 @@
package rui
import (
"sort"
"strconv"
"strings"
)
const (
defaultMedia = 0
portraitMedia = 1
landscapeMedia = 2
)
type mediaStyle struct {
orientation int
width int
height int
styles map[string]DataObject
}
func (rule mediaStyle) cssText() string {
builder := allocStringBuilder()
defer freeStringBuilder(builder)
switch rule.orientation {
case portraitMedia:
builder.WriteString(" and (orientation: portrait)")
case landscapeMedia:
builder.WriteString(" and (orientation: landscape)")
}
if rule.width > 0 {
builder.WriteString(" and (max-width: ")
builder.WriteString(strconv.Itoa(rule.width))
builder.WriteString("px)")
}
if rule.height > 0 {
builder.WriteString(" and (max-height: ")
builder.WriteString(strconv.Itoa(rule.height))
builder.WriteString("px)")
}
return builder.String()
}
func parseMediaRule(text string) (mediaStyle, bool) {
rule := mediaStyle{orientation: defaultMedia, width: 0, height: 0, styles: map[string]DataObject{}}
elements := strings.Split(text, ":")
for i := 1; i < len(elements); i++ {
switch element := elements[i]; element {
case "portrait":
if rule.orientation != defaultMedia {
ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`)
return rule, false
}
rule.orientation = portraitMedia
case "landscape":
if rule.orientation != defaultMedia {
ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`)
return rule, false
}
rule.orientation = landscapeMedia
default:
elementSize := func(name string) (int, bool) {
if strings.HasPrefix(element, name) {
size, err := strconv.Atoi(element[len(name):])
if err == nil && size > 0 {
return size, true
}
ErrorLogF(`Invalid style section name "%s": %s`, text, err.Error())
return 0, false
}
return 0, true
}
if size, ok := elementSize("width"); !ok || size > 0 {
if !ok {
return rule, false
}
if rule.width != 0 {
ErrorLog(`Duplicate "width" tag in the style section "` + text + `"`)
return rule, false
}
rule.width = size
} else if size, ok := elementSize("height"); !ok || size > 0 {
if !ok {
return rule, false
}
if rule.height != 0 {
ErrorLog(`Duplicate "height" tag in the style section "` + text + `"`)
return rule, false
}
rule.height = size
} else {
ErrorLogF(`Unknown elemnet "%s" in the style section name "%s"`, element, text)
return rule, false
}
}
}
return rule, true
}
type theme struct {
name string
constants map[string]string
touchConstants map[string]string
colors map[string]string
darkColors map[string]string
styles map[string]DataObject
mediaStyles []mediaStyle
}
var defaultTheme = new(theme)
func newTheme(text string) (*theme, bool) {
result := new(theme)
result.init()
ok := result.addText(text)
return result, ok
}
func (theme *theme) init() {
theme.constants = map[string]string{}
theme.touchConstants = map[string]string{}
theme.colors = map[string]string{}
theme.darkColors = map[string]string{}
theme.styles = map[string]DataObject{}
theme.mediaStyles = []mediaStyle{}
}
func (theme *theme) concat(anotherTheme *theme) {
if theme.constants == nil {
theme.init()
}
for tag, constant := range anotherTheme.constants {
theme.constants[tag] = constant
}
for tag, constant := range anotherTheme.touchConstants {
theme.touchConstants[tag] = constant
}
for tag, color := range anotherTheme.colors {
theme.colors[tag] = color
}
for tag, color := range anotherTheme.darkColors {
theme.darkColors[tag] = color
}
for tag, style := range anotherTheme.styles {
theme.styles[tag] = style
}
for _, anotherMedia := range anotherTheme.mediaStyles {
exists := false
for _, media := range theme.mediaStyles {
if anotherMedia.height == media.height &&
anotherMedia.width == media.width &&
anotherMedia.orientation == media.orientation {
for tag, style := range anotherMedia.styles {
media.styles[tag] = style
}
exists = true
break
}
}
if !exists {
theme.mediaStyles = append(theme.mediaStyles, anotherMedia)
}
}
}
func (theme *theme) cssText(session Session) string {
if theme.styles == nil {
theme.init()
return ""
}
var builder cssStyleBuilder
builder.init()
for tag, obj := range theme.styles {
var style viewStyle
style.init()
parseProperties(&style, obj)
builder.startStyle(tag)
style.cssViewStyle(&builder, session, nil)
builder.endStyle()
}
for _, media := range theme.mediaStyles {
builder.startMedia(media.cssText())
for tag, obj := range media.styles {
var style viewStyle
style.init()
parseProperties(&style, obj)
builder.startStyle(tag)
style.cssViewStyle(&builder, session, nil)
builder.endStyle()
}
builder.endMedia()
}
return builder.finish()
}
func (theme *theme) addText(themeText string) bool {
data := ParseDataText(themeText)
if data == nil {
return false
}
theme.addData(data)
return true
}
func (theme *theme) addData(data DataObject) {
if theme.constants == nil {
theme.init()
}
if data.IsObject() && data.Tag() == "theme" {
theme.parseThemeData(data)
}
}
func (theme *theme) parseThemeData(data DataObject) {
count := data.PropertyCount()
for i := 0; i < count; i++ {
if d := data.Property(i); d != nil {
switch tag := d.Tag(); tag {
case "constants":
if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.constants[prop.Tag()] = prop.Text()
}
}
}
}
case "constants:touch":
if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.touchConstants[prop.Tag()] = prop.Text()
}
}
}
}
case "colors":
if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.colors[prop.Tag()] = prop.Text()
}
}
}
}
case "colors:dark":
if d.Type() == ObjectNode {
if obj := d.Object(); obj != nil {
objCount := obj.PropertyCount()
for k := 0; k < objCount; k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
theme.darkColors[prop.Tag()] = prop.Text()
}
}
}
}
case "styles":
if d.Type() == ArrayNode {
arraySize := d.ArraySize()
for k := 0; k < arraySize; k++ {
if element := d.ArrayElement(k); element != nil && element.IsObject() {
if obj := element.Object(); obj != nil {
theme.styles[obj.Tag()] = obj
}
}
}
}
default:
if d.Type() == ArrayNode && strings.HasPrefix(tag, "styles:") {
if rule, ok := parseMediaRule(tag); ok {
arraySize := d.ArraySize()
for k := 0; k < arraySize; k++ {
if element := d.ArrayElement(k); element != nil && element.IsObject() {
if obj := element.Object(); obj != nil {
rule.styles[obj.Tag()] = obj
}
}
}
theme.mediaStyles = append(theme.mediaStyles, rule)
}
}
}
}
}
if len(theme.mediaStyles) > 0 {
sort.SliceStable(theme.mediaStyles, func(i, j int) bool {
if theme.mediaStyles[i].orientation != theme.mediaStyles[j].orientation {
return theme.mediaStyles[i].orientation < theme.mediaStyles[j].orientation
}
if theme.mediaStyles[i].width != theme.mediaStyles[j].width {
return theme.mediaStyles[i].width < theme.mediaStyles[j].width
}
return theme.mediaStyles[i].height < theme.mediaStyles[j].height
})
}
}

410
timePicker.go Normal file
View File

@ -0,0 +1,410 @@
package rui
import (
"fmt"
"strconv"
"strings"
"time"
)
const (
TimeChangedEvent = "time-changed"
TimePickerMin = "time-picker-min"
TimePickerMax = "time-picker-max"
TimePickerStep = "time-picker-step"
TimePickerValue = "time-picker-value"
timeFormat = "15:04"
)
// TimePicker - TimePicker view
type TimePicker interface {
View
}
type timePickerData struct {
viewData
timeChangedListeners []func(TimePicker, time.Time)
}
// NewTimePicker create new TimePicker object and return it
func NewTimePicker(session Session, params Params) TimePicker {
view := new(timePickerData)
view.Init(session)
setInitParams(view, params)
return view
}
func newTimePicker(session Session) View {
return NewTimePicker(session, nil)
}
func (picker *timePickerData) Init(session Session) {
picker.viewData.Init(session)
picker.tag = "TimePicker"
picker.timeChangedListeners = []func(TimePicker, time.Time){}
}
func (picker *timePickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case Type, Min, Max, Step, Value:
return "time-picker-" + tag
}
return tag
}
func (picker *timePickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *timePickerData) remove(tag string) {
switch tag {
case TimeChangedEvent:
if len(picker.timeChangedListeners) > 0 {
picker.timeChangedListeners = []func(TimePicker, time.Time){}
}
case TimePickerMin:
delete(picker.properties, TimePickerMin)
removeProperty(picker.htmlID(), Min, picker.session)
case TimePickerMax:
delete(picker.properties, TimePickerMax)
removeProperty(picker.htmlID(), Max, picker.session)
case TimePickerStep:
delete(picker.properties, TimePickerMax)
removeProperty(picker.htmlID(), Step, picker.session)
case TimePickerValue:
delete(picker.properties, TimePickerValue)
updateProperty(picker.htmlID(), Value, time.Now().Format(timeFormat), picker.session)
default:
picker.viewData.remove(tag)
picker.propertyChanged(tag)
}
}
func (picker *timePickerData) Set(tag string, value interface{}) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *timePickerData) set(tag string, value interface{}) bool {
if value == nil {
picker.remove(tag)
return true
}
setTimeValue := func(tag string) (time.Time, bool) {
switch value := value.(type) {
case time.Time:
picker.properties[tag] = value
return value, true
case string:
if text, ok := picker.Session().resolveConstants(value); ok {
if time, err := time.Parse(timeFormat, text); err == nil {
picker.properties[tag] = value
return time, true
}
}
}
notCompatibleType(tag, value)
return time.Now(), false
}
switch tag {
case TimePickerMin:
old, oldOK := getTimeProperty(picker, TimePickerMin, Min)
if time, ok := setTimeValue(TimePickerMin); ok {
if !oldOK || time != old {
updateProperty(picker.htmlID(), Min, time.Format(timeFormat), picker.session)
}
return true
}
case TimePickerMax:
old, oldOK := getTimeProperty(picker, TimePickerMax, Max)
if time, ok := setTimeValue(TimePickerMax); ok {
if !oldOK || time != old {
updateProperty(picker.htmlID(), Max, time.Format(timeFormat), picker.session)
}
return true
}
case TimePickerStep:
oldStep := GetTimePickerStep(picker, "")
if picker.setIntProperty(TimePickerStep, value) {
step := GetTimePickerStep(picker, "")
if oldStep != step {
if step > 0 {
updateProperty(picker.htmlID(), Step, strconv.Itoa(step), picker.session)
} else {
removeProperty(picker.htmlID(), Step, picker.session)
}
}
return true
}
case TimePickerValue:
oldTime := GetTimePickerValue(picker, "")
if time, ok := setTimeValue(TimePickerMax); ok {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), time.Format(timeFormat)))
if time != oldTime {
for _, listener := range picker.timeChangedListeners {
listener(picker, time)
}
}
return true
}
case TimeChangedEvent:
switch value := value.(type) {
case func(TimePicker, time.Time):
picker.timeChangedListeners = []func(TimePicker, time.Time){value}
case func(time.Time):
fn := func(view TimePicker, date time.Time) {
value(date)
}
picker.timeChangedListeners = []func(TimePicker, time.Time){fn}
case []func(TimePicker, time.Time):
picker.timeChangedListeners = value
case []func(time.Time):
listeners := make([]func(TimePicker, time.Time), len(value))
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
listeners[i] = func(view TimePicker, date time.Time) {
val(date)
}
}
picker.timeChangedListeners = listeners
case []interface{}:
listeners := make([]func(TimePicker, time.Time), len(value))
for i, val := range value {
if val == nil {
notCompatibleType(tag, val)
return false
}
switch val := val.(type) {
case func(TimePicker, time.Time):
listeners[i] = val
case func(time.Time):
listeners[i] = func(view TimePicker, date time.Time) {
val(date)
}
default:
notCompatibleType(tag, val)
return false
}
}
picker.timeChangedListeners = listeners
}
return true
default:
if picker.viewData.set(tag, value) {
picker.propertyChanged(tag)
return true
}
}
return false
}
func (picker *timePickerData) Get(tag string) interface{} {
return picker.get(picker.normalizeTag(tag))
}
func (picker *timePickerData) get(tag string) interface{} {
switch tag {
case TimeChangedEvent:
return picker.timeChangedListeners
default:
return picker.viewData.get(tag)
}
}
func (picker *timePickerData) htmlTag() string {
return "input"
}
func (picker *timePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer)
buffer.WriteString(` type="time"`)
if min, ok := getTimeProperty(picker, TimePickerMin, Min); ok {
buffer.WriteString(` min="`)
buffer.WriteString(min.Format(timeFormat))
buffer.WriteByte('"')
}
if max, ok := getTimeProperty(picker, TimePickerMax, Max); ok {
buffer.WriteString(` max="`)
buffer.WriteString(max.Format(timeFormat))
buffer.WriteByte('"')
}
if step, ok := intProperty(picker, TimePickerStep, picker.Session(), 0); ok && step > 0 {
buffer.WriteString(` step="`)
buffer.WriteString(strconv.Itoa(step))
buffer.WriteByte('"')
}
buffer.WriteString(` value="`)
buffer.WriteString(GetTimePickerValue(picker, "").Format(timeFormat))
buffer.WriteByte('"')
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
}
func (picker *timePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *timePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
if value, err := time.Parse(timeFormat, text); err == nil {
oldValue := GetTimePickerValue(picker, "")
picker.properties[TimePickerValue] = value
if value != oldValue {
for _, listener := range picker.timeChangedListeners {
listener(picker, value)
}
}
}
}
return true
}
return picker.viewData.handleCommand(self, command, data)
}
func getTimeProperty(view View, mainTag, shortTag string) (time.Time, bool) {
valueToTime := func(value interface{}) (time.Time, bool) {
if value != nil {
switch value := value.(type) {
case time.Time:
return value, true
case string:
if text, ok := view.Session().resolveConstants(value); ok {
if result, err := time.Parse(timeFormat, text); err == nil {
return result, true
}
}
}
}
return time.Now(), false
}
if view != nil {
if result, ok := valueToTime(view.getRaw(mainTag)); ok {
return result, true
}
if value, ok := valueFromStyle(view, shortTag); ok {
if result, ok := valueToTime(value); ok {
return result, true
}
}
}
return time.Now(), false
}
// GetTimePickerMin returns the min time of TimePicker subview and "true" as the second value if the min time is set,
// "false" as the second value otherwise.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTimePickerMin(view View, subviewID string) (time.Time, bool) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
return getTimeProperty(view, TimePickerMin, Min)
}
return time.Now(), false
}
// GetTimePickerMax returns the max time of TimePicker subview and "true" as the second value if the min time is set,
// "false" as the second value otherwise.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTimePickerMax(view View, subviewID string) (time.Time, bool) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
return getTimeProperty(view, TimePickerMax, Max)
}
return time.Now(), false
}
// GetTimePickerStep returns the time changing step in seconds of TimePicker subview.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTimePickerStep(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return 60
}
result, ok := intStyledProperty(view, TimePickerStep, 60)
if !ok {
result, _ = intStyledProperty(view, Step, 60)
}
if result < 0 {
return 60
}
return result
}
// GetTimePickerValue returns the time of TimePicker subview.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTimePickerValue(view View, subviewID string) time.Time {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view == nil {
return time.Now()
}
time, _ := getTimeProperty(view, TimePickerValue, Value)
return time
}
// GetTimeChangedListeners returns the TimeChangedListener list of an TimePicker subview.
// If there are no listeners then the empty list is returned
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTimeChangedListeners(view View, subviewID string) []func(TimePicker, time.Time) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(TimeChangedEvent); value != nil {
if listeners, ok := value.([]func(TimePicker, time.Time)); ok {
return listeners
}
}
}
return []func(TimePicker, time.Time){}
}

347
touchEvents.go Normal file
View File

@ -0,0 +1,347 @@
package rui
import (
"strconv"
"strings"
)
const (
// TouchStart is the constant for "touch-start" property tag.
// The "touch-start" event is fired when one or more touch points are placed on the touch surface.
// The main listener format: func(View, TouchEvent).
// The additional listener formats: func(TouchEvent), func(View), and func().
TouchStart = "touch-start"
// TouchEnd is the constant for "touch-end" property tag.
// The "touch-end" event fires when one or more touch points are removed from the touch surface.
// The main listener format: func(View, TouchEvent).
// The additional listener formats: func(TouchEvent), func(View), and func().
TouchEnd = "touch-end"
// TouchMove is the constant for "touch-move" property tag.
// The "touch-move" event is fired when one or more touch points are moved along the touch surface.
// The main listener format: func(View, TouchEvent).
// The additional listener formats: func(TouchEvent), func(View), and func().
TouchMove = "touch-move"
// TouchCancel is the constant for "touch-cancel" property tag.
// The "touch-cancel" event is fired when one or more touch points have been disrupted
// in an implementation-specific manner (for example, too many touch points are created).
// The main listener format: func(View, TouchEvent).
// The additional listener formats: func(TouchEvent), func(View), and func().
TouchCancel = "touch-cancel"
)
// Touch contains parameters of a single touch of a touch event
type Touch struct {
// Identifier is a unique identifier for this Touch object. A given touch point (say, by a finger)
// will have the same identifier for the duration of its movement around the surface.
// This lets you ensure that you're tracking the same touch all the time.
Identifier int
// X provides the horizontal coordinate within the view's viewport.
X float64
// Y provides the vertical coordinate within the view's viewport.
Y float64
// ClientX provides the horizontal coordinate within the application's viewport at which the event occurred.
ClientX float64
// ClientY provides the vertical coordinate within the application's viewport at which the event occurred.
ClientY float64
// ScreenX provides the horizontal coordinate (offset) of the touch pointer in global (screen) coordinates.
ScreenX float64
// ScreenY provides the vertical coordinate (offset) of the touch pointer in global (screen) coordinates.
ScreenY float64
// RadiusX is the X radius of the ellipse that most closely circumscribes the area of contact with the screen.
// The value is in pixels of the same scale as screenX.
RadiusX float64
// RadiusY is the Y radius of the ellipse that most closely circumscribes the area of contact with the screen.
// The value is in pixels of the same scale as screenX.
RadiusY float64
// RotationAngle is the angle (in degrees) that the ellipse described by radiusX and radiusY must be rotated,
// clockwise, to most accurately cover the area of contact between the user and the surface.
RotationAngle float64
// Force is the amount of pressure being applied to the surface by the user, as a float
// between 0.0 (no pressure) and 1.0 (maximum pressure).
Force float64
}
// TouchEvent contains parameters of a touch event
type TouchEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary.
TimeStamp uint64
// Touches is the array of all the Touch objects representing all current points
// of contact with the surface, regardless of target or changed status.
Touches []Touch
// CtrlKey == true if the control key was down when the event was fired. false otherwise.
CtrlKey bool
// ShiftKey == true if the shift key was down when the event was fired. false otherwise.
ShiftKey bool
// AltKey == true if the alt key was down when the event was fired. false otherwise.
AltKey bool
// MetaKey == true if the meta key was down when the event was fired. false otherwise.
MetaKey bool
}
func valueToTouchListeners(value interface{}) ([]func(View, TouchEvent), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(View, TouchEvent):
return []func(View, TouchEvent){value}, true
case func(TouchEvent):
fn := func(view View, event TouchEvent) {
value(event)
}
return []func(View, TouchEvent){fn}, true
case func(View):
fn := func(view View, event TouchEvent) {
value(view)
}
return []func(View, TouchEvent){fn}, true
case func():
fn := func(view View, event TouchEvent) {
value()
}
return []func(View, TouchEvent){fn}, true
case []func(View, TouchEvent):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(TouchEvent):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, TouchEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event TouchEvent) {
v(event)
}
}
return listeners, true
case []func(View):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, TouchEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event TouchEvent) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, TouchEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view View, event TouchEvent) {
v()
}
}
return listeners, true
case []interface{}:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(View, TouchEvent), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(View, TouchEvent):
listeners[i] = v
case func(TouchEvent):
listeners[i] = func(view View, event TouchEvent) {
v(event)
}
case func(View):
listeners[i] = func(view View, event TouchEvent) {
v(view)
}
case func():
listeners[i] = func(view View, event TouchEvent) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var touchEvents = map[string]struct{ jsEvent, jsFunc string }{
TouchStart: {jsEvent: "ontouchstart", jsFunc: "touchStartEvent"},
TouchEnd: {jsEvent: "ontouchend", jsFunc: "touchEndEvent"},
TouchMove: {jsEvent: "ontouchmove", jsFunc: "touchMoveEvent"},
TouchCancel: {jsEvent: "ontouchcancel", jsFunc: "touchCancelEvent"},
}
func (view *viewData) setTouchListener(tag string, value interface{}) bool {
listeners, ok := valueToTouchListeners(value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeTouchListener(tag)
} else if js, ok := touchEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeTouchListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := touchEvents[tag]; ok {
updateProperty(view.htmlID(), js.jsEvent, "", view.Session())
}
}
}
func getTouchListeners(view View, subviewID string, tag string) []func(View, TouchEvent) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(View, TouchEvent)); ok {
return result
}
}
}
return []func(View, TouchEvent){}
}
func touchEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range touchEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, TouchEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func (event *TouchEvent) init(data DataObject) {
event.Touches = []Touch{}
event.TimeStamp = getTimeStamp(data)
if node := data.PropertyWithTag("touches"); node != nil && node.Type() == ArrayNode {
for i := 0; i < node.ArraySize(); i++ {
if element := node.ArrayElement(i); element != nil && element.IsObject() {
if obj := element.Object(); obj != nil {
var touch Touch
if value, ok := obj.PropertyValue("identifier"); ok {
touch.Identifier, _ = strconv.Atoi(value)
}
touch.X = dataFloatProperty(obj, "x")
touch.Y = dataFloatProperty(obj, "y")
touch.ClientX = dataFloatProperty(obj, "clientX")
touch.ClientY = dataFloatProperty(obj, "clientY")
touch.ScreenX = dataFloatProperty(obj, "screenX")
touch.ScreenY = dataFloatProperty(obj, "screenY")
touch.RadiusX = dataFloatProperty(obj, "radiusX")
touch.RadiusY = dataFloatProperty(obj, "radiusY")
touch.RotationAngle = dataFloatProperty(obj, "rotationAngle")
touch.Force = dataFloatProperty(obj, "force")
event.Touches = append(event.Touches, touch)
}
}
}
}
event.CtrlKey = dataBoolProperty(data, "ctrlKey")
event.ShiftKey = dataBoolProperty(data, "shiftKey")
event.AltKey = dataBoolProperty(data, "altKey")
event.MetaKey = dataBoolProperty(data, "metaKey")
}
func handleTouchEvents(view View, tag string, data DataObject) {
listeners := getTouchListeners(view, "", tag)
if len(listeners) == 0 {
return
}
var event TouchEvent
event.init(data)
for _, listener := range listeners {
listener(view, event)
}
}
// GetTouchStartListeners returns the "touch-start" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTouchStartListeners(view View, subviewID string) []func(View, TouchEvent) {
return getTouchListeners(view, subviewID, TouchStart)
}
// GetTouchEndListeners returns the "touch-end" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTouchEndListeners(view View, subviewID string) []func(View, TouchEvent) {
return getTouchListeners(view, subviewID, TouchEnd)
}
// GetTouchMoveListeners returns the "touch-move" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTouchMoveListeners(view View, subviewID string) []func(View, TouchEvent) {
return getTouchListeners(view, subviewID, TouchMove)
}
// GetTouchCancelListeners returns the "touch-cancel" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTouchCancelListeners(view View, subviewID string) []func(View, TouchEvent) {
return getTouchListeners(view, subviewID, TouchCancel)
}

78
utils.go Normal file
View File

@ -0,0 +1,78 @@
package rui
import (
"net"
"strconv"
"strings"
)
var stringBuilders []*strings.Builder = make([]*strings.Builder, 4096)
var stringBuilderCount = 0
func allocStringBuilder() *strings.Builder {
for stringBuilderCount > 0 {
stringBuilderCount--
result := stringBuilders[stringBuilderCount]
if result != nil {
stringBuilders[stringBuilderCount] = nil
result.Reset()
return result
}
}
result := new(strings.Builder)
result.Grow(4096)
return result
}
func freeStringBuilder(builder *strings.Builder) {
if builder != nil {
if stringBuilderCount == len(stringBuilders) {
stringBuilders = append(stringBuilders, builder)
} else {
stringBuilders[stringBuilderCount] = builder
}
stringBuilderCount++
}
}
func GetLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
}
for _, address := range addrs {
// check the address type and if it is not a loopback the display it
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
return ipnet.IP.String()
}
}
}
return "localhost"
}
func dataIntProperty(data DataObject, tag string) int {
if value, ok := data.PropertyValue(tag); ok {
if n, err := strconv.Atoi(value); err == nil {
return n
}
}
return 0
}
func dataBoolProperty(data DataObject, tag string) bool {
if value, ok := data.PropertyValue(tag); ok && value == "1" {
return true
}
return false
}
func dataFloatProperty(data DataObject, tag string) float64 {
if value, ok := data.PropertyValue(tag); ok {
if n, err := strconv.ParseFloat(value, 64); err == nil {
return n
}
}
return 0
}

134
videoPlayer.go Normal file
View File

@ -0,0 +1,134 @@
package rui
import (
"fmt"
"strings"
)
const (
// VideoWidth is the constant for the "video-width" property tag of VideoPlayer.
// The "video-width" float property defines the width of the video's display area in pixels.
VideoWidth = "video-width"
// VideoHeight is the constant for the "video-height" property tag of VideoPlayer.
// The "video-height" float property defines the height of the video's display area in pixels.
VideoHeight = "video-height"
// Poster is the constant for the "poster" property tag of VideoPlayer.
// The "poster" property defines an URL for an image to be shown while the video is downloading.
// If this attribute isn't specified, nothing is displayed until the first frame is available,
// then the first frame is shown as the poster frame.
Poster = "poster"
)
type VideoPlayer interface {
MediaPlayer
}
type videoPlayerData struct {
mediaPlayerData
}
// NewVideoPlayer create new MediaPlayer object and return it
func NewVideoPlayer(session Session, params Params) MediaPlayer {
view := new(videoPlayerData)
view.Init(session)
view.tag = "VideoPlayer"
setInitParams(view, params)
return view
}
func newVideoPlayer(session Session) View {
return NewVideoPlayer(session, nil)
}
func (player *videoPlayerData) Init(session Session) {
player.mediaPlayerData.Init(session)
player.tag = "VideoPlayer"
}
func (player *videoPlayerData) htmlTag() string {
return "video"
}
func (player *videoPlayerData) Remove(tag string) {
player.remove(strings.ToLower(tag))
}
func (player *videoPlayerData) remove(tag string) {
switch tag {
case VideoWidth:
delete(player.properties, tag)
removeProperty(player.htmlID(), "width", player.Session())
case VideoHeight:
delete(player.properties, tag)
removeProperty(player.htmlID(), "height", player.Session())
case Poster:
delete(player.properties, tag)
removeProperty(player.htmlID(), Poster, player.Session())
default:
player.mediaPlayerData.remove(tag)
}
}
func (player *videoPlayerData) Set(tag string, value interface{}) bool {
return player.set(strings.ToLower(tag), value)
}
func (player *videoPlayerData) set(tag string, value interface{}) bool {
if value == nil {
player.remove(tag)
return true
}
if player.mediaPlayerData.set(tag, value) {
session := player.Session()
updateSize := func(cssTag string) {
if size, ok := floatProperty(player, tag, session, 0); ok {
if size > 0 {
updateProperty(player.htmlID(), cssTag, fmt.Sprintf("%g", size), session)
} else {
removeProperty(player.htmlID(), cssTag, session)
}
}
}
switch tag {
case VideoWidth:
updateSize("width")
case VideoHeight:
updateSize("height")
case Poster:
if url, ok := stringProperty(player, Poster, session); ok {
updateProperty(player.htmlID(), Poster, url, session)
}
}
return true
}
return false
}
func (player *videoPlayerData) htmlProperties(self View, buffer *strings.Builder) {
player.mediaPlayerData.htmlProperties(self, buffer)
session := player.Session()
if size, ok := floatProperty(player, VideoWidth, session, 0); ok && size > 0 {
buffer.WriteString(fmt.Sprintf(` width="%g"`, size))
}
if size, ok := floatProperty(player, VideoHeight, session, 0); ok && size > 0 {
buffer.WriteString(fmt.Sprintf(` height="%g"`, size))
}
if url, ok := stringProperty(player, Poster, session); ok && url != "" {
buffer.WriteString(` poster="`)
buffer.WriteString(url)
buffer.WriteString(`"`)
}
}

760
view.go Normal file
View File

@ -0,0 +1,760 @@
package rui
import (
"fmt"
"sort"
"strconv"
"strings"
)
// Frame - the location and size of a rectangle area
type Frame struct {
// Left - the left border
Left float64
// Top - the top border
Top float64
// Width - the width of a rectangle area
Width float64
// Height - the height of a rectangle area
Height float64
}
// Right returns the right border
func (frame Frame) Right() float64 {
return frame.Left + frame.Width
}
// Bottom returns the bottom border
func (frame Frame) Bottom() float64 {
return frame.Top + frame.Height
}
// Params defines a type of a parameters list
type Params map[string]interface{}
func (params Params) AllTags() []string {
tags := make([]string, 0, len(params))
for t := range params {
tags = append(tags, t)
}
sort.Strings(tags)
return tags
}
// View - base view interface
type View interface {
Properties
fmt.Stringer
ruiStringer
// Init initializes fields of View by default values
Init(session Session)
// Session returns the current Session interface
Session() Session
// Parent returns the parent view
Parent() View
parentHTMLID() string
setParentID(parentID string)
// Tag returns the tag of View interface
Tag() string
// ID returns the id of the view
ID() string
// Focusable returns true if the view receives the focus
Focusable() bool
// Frame returns the location and size of the view in pixels
Frame() Frame
// Scroll returns the location size of the scrolable view in pixels
Scroll() Frame
// SetAnimated sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log
SetAnimated(tag string, value interface{}, animation Animation) bool
handleCommand(self View, command string, data DataObject) bool
//updateEventHandlers()
htmlClass(disabled bool) string
htmlTag() string
closeHTMLTag() bool
htmlID() string
htmlSubviews(self View, buffer *strings.Builder)
htmlProperties(self View, buffer *strings.Builder)
htmlDisabledProperties(self View, buffer *strings.Builder)
cssStyle(self View, builder cssBuilder)
addToCSSStyle(addCSS map[string]string)
onResize(self View, x, y, width, height float64)
onItemResize(self View, index int, x, y, width, height float64)
setNoResizeEvent()
isNoResizeEvent() bool
setScroll(x, y, width, height float64)
}
// viewData - base implementation of View interface
type viewData struct {
viewStyle
session Session
tag string
viewID string
_htmlID string
parentID string
systemClass string
animation map[string]Animation
addCSS map[string]string
frame Frame
scroll Frame
noResizeEvent bool
created bool
//animation map[string]AnimationEndListener
}
func newView(session Session) View {
view := new(viewData)
view.Init(session)
return view
}
func setInitParams(view View, params Params) {
if params != nil {
session := view.Session()
if !session.ignoreViewUpdates() {
session.setIgnoreViewUpdates(true)
defer session.setIgnoreViewUpdates(false)
}
for _, tag := range params.AllTags() {
if value, ok := params[tag]; ok {
view.Set(tag, value)
}
}
}
}
// NewView create new View object and return it
func NewView(session Session, params Params) View {
view := new(viewData)
view.Init(session)
setInitParams(view, params)
return view
}
func (view *viewData) Init(session Session) {
view.viewStyle.init()
view.tag = "View"
view.session = session
view.addCSS = map[string]string{}
//view.animation = map[string]AnimationEndListener{}
view.animation = map[string]Animation{}
view.noResizeEvent = false
view.created = false
}
func (view *viewData) Session() Session {
return view.session
}
func (view *viewData) Parent() View {
return view.session.viewByHTMLID(view.parentID)
}
func (view *viewData) parentHTMLID() string {
return view.parentID
}
func (view *viewData) setParentID(parentID string) {
view.parentID = parentID
}
func (view *viewData) Tag() string {
return view.tag
}
func (view *viewData) ID() string {
return view.viewID
}
func (view *viewData) ViewByID(id string) View {
if id == view.ID() {
if v := view.session.viewByHTMLID(view.htmlID()); v != nil {
return v
}
return view
}
return nil
}
func (view *viewData) Focusable() bool {
return false
}
func (view *viewData) Remove(tag string) {
view.remove(strings.ToLower(tag))
}
func (view *viewData) remove(tag string) {
switch tag {
case ID:
view.viewID = ""
case Style, StyleDisabled:
if _, ok := view.properties[tag]; ok {
delete(view.properties, tag)
updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view)), view.session)
}
case FocusEvent, LostFocusEvent:
view.removeFocusListener(tag)
case KeyDownEvent, KeyUpEvent:
view.removeKeyListener(tag)
case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent:
view.removeMouseListener(tag)
case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel:
view.removePointerListener(tag)
case TouchStart, TouchEnd, TouchMove, TouchCancel:
view.removeTouchListener(tag)
case ResizeEvent, ScrollEvent:
delete(view.properties, tag)
case Content:
if _, ok := view.properties[Content]; ok {
delete(view.properties, Content)
updateInnerHTML(view.htmlID(), view.session)
}
default:
view.viewStyle.remove(tag)
view.propertyChanged(tag)
}
}
func (view *viewData) Set(tag string, value interface{}) bool {
return view.set(strings.ToLower(tag), value)
}
func (view *viewData) set(tag string, value interface{}) bool {
if value == nil {
view.remove(tag)
return true
}
switch tag {
case ID:
if text, ok := value.(string); ok {
view.viewID = text
return true
}
notCompatibleType(ID, value)
return false
case Style, StyleDisabled:
if text, ok := value.(string); ok {
view.properties[tag] = text
//updateInnerHTML(view.parentID, view.session)
if view.created {
updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view)), view.session)
}
return true
}
notCompatibleType(ID, value)
return false
case FocusEvent, LostFocusEvent:
return view.setFocusListener(tag, value)
case KeyDownEvent, KeyUpEvent:
return view.setKeyListener(tag, value)
case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent:
return view.setMouseListener(tag, value)
case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel:
return view.setPointerListener(tag, value)
case TouchStart, TouchEnd, TouchMove, TouchCancel:
return view.setTouchListener(tag, value)
case ResizeEvent, ScrollEvent:
return view.setFrameListener(tag, value)
}
if view.viewStyle.set(tag, value) {
if view.created {
view.propertyChanged(tag)
}
return true
}
return false
}
func (view *viewData) propertyChanged(tag string) {
if view.updateTransformProperty(tag) {
return
}
htmlID := view.htmlID()
session := view.session
switch tag {
case Disabled:
updateInnerHTML(view.parentHTMLID(), session)
case Background:
updateCSSProperty(htmlID, Background, view.backgroundCSS(view), session)
return
case Border:
if getBorder(view, Border) == nil {
updateCSSProperty(htmlID, BorderWidth, "", session)
updateCSSProperty(htmlID, BorderColor, "", session)
updateCSSProperty(htmlID, BorderStyle, "none", session)
return
}
fallthrough
case BorderLeft, BorderRight, BorderTop, BorderBottom:
if border := getBorder(view, Border); border != nil {
updateCSSProperty(htmlID, BorderWidth, border.cssWidthValue(session), session)
updateCSSProperty(htmlID, BorderColor, border.cssColorValue(session), session)
updateCSSProperty(htmlID, BorderStyle, border.cssStyleValue(session), session)
}
return
case BorderStyle, BorderLeftStyle, BorderRightStyle, BorderTopStyle, BorderBottomStyle:
if border := getBorder(view, Border); border != nil {
updateCSSProperty(htmlID, BorderStyle, border.cssStyleValue(session), session)
}
return
case BorderColor, BorderLeftColor, BorderRightColor, BorderTopColor, BorderBottomColor:
if border := getBorder(view, Border); border != nil {
updateCSSProperty(htmlID, BorderColor, border.cssColorValue(session), session)
}
return
case BorderWidth, BorderLeftWidth, BorderRightWidth, BorderTopWidth, BorderBottomWidth:
if border := getBorder(view, Border); border != nil {
updateCSSProperty(htmlID, BorderWidth, border.cssWidthValue(session), session)
}
return
case Outline, OutlineColor, OutlineStyle, OutlineWidth:
updateCSSProperty(htmlID, Outline, GetOutline(view, "").cssString(), session)
return
case Shadow:
updateCSSProperty(htmlID, "box-shadow", shadowCSS(view, Shadow, session), session)
return
case TextShadow:
updateCSSProperty(htmlID, "text-shadow", shadowCSS(view, TextShadow, session), session)
return
case Radius, RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
radius := GetRadius(view, "")
updateCSSProperty(htmlID, "border-radius", radius.cssString(), session)
return
case Margin, MarginTop, MarginRight, MarginBottom, MarginLeft,
"top-margin", "right-margin", "bottom-margin", "left-margin":
margin := GetMargin(view, "")
updateCSSProperty(htmlID, Margin, margin.cssString(), session)
return
case Padding, PaddingTop, PaddingRight, PaddingBottom, PaddingLeft,
"top-padding", "right-padding", "bottom-padding", "left-padding":
padding := GetPadding(view, "")
updateCSSProperty(htmlID, Padding, padding.cssString(), session)
return
case AvoidBreak:
if avoid, ok := boolProperty(view, AvoidBreak, session); ok {
if avoid {
updateCSSProperty(htmlID, "break-inside", "avoid", session)
} else {
updateCSSProperty(htmlID, "break-inside", "auto", session)
}
}
return
case Clip:
if clip := getClipShape(view, Clip, session); clip != nil && clip.valid(session) {
updateCSSProperty(htmlID, `clip-path`, clip.cssStyle(session), session)
} else {
updateCSSProperty(htmlID, `clip-path`, "none", session)
}
return
case ShapeOutside:
if clip := getClipShape(view, ShapeOutside, session); clip != nil && clip.valid(session) {
updateCSSProperty(htmlID, ShapeOutside, clip.cssStyle(session), session)
} else {
updateCSSProperty(htmlID, ShapeOutside, "none", session)
}
return
case Filter:
text := ""
if value := view.getRaw(Filter); value != nil {
if filter, ok := value.(ViewFilter); ok {
text = filter.cssStyle(session)
}
}
updateCSSProperty(htmlID, Filter, text, session)
return
case FontName:
if font, ok := stringProperty(view, FontName, session); ok {
updateCSSProperty(htmlID, "font-family", font, session)
} else {
updateCSSProperty(htmlID, "font-family", "", session)
}
return
case Italic:
if state, ok := boolProperty(view, tag, session); ok {
if state {
updateCSSProperty(htmlID, "font-style", "italic", session)
} else {
updateCSSProperty(htmlID, "font-style", "normal", session)
}
} else {
updateCSSProperty(htmlID, "font-style", "", session)
}
case SmallCaps:
if state, ok := boolProperty(view, tag, session); ok {
if state {
updateCSSProperty(htmlID, "font-variant", "small-caps", session)
} else {
updateCSSProperty(htmlID, "font-variant", "normal", session)
}
} else {
updateCSSProperty(htmlID, "font-variant", "", session)
}
case Strikethrough, Overline, Underline:
updateCSSProperty(htmlID, "text-decoration", view.cssTextDecoration(session), session)
for _, tag2 := range []string{TextLineColor, TextLineStyle, TextLineThickness} {
view.propertyChanged(tag2)
}
}
if cssTag, ok := sizeProperties[tag]; ok {
size, _ := sizeProperty(view, tag, session)
updateCSSProperty(htmlID, cssTag, size.cssString(""), session)
return
}
colorTags := map[string]string{
BackgroundColor: BackgroundColor,
TextColor: "color",
TextLineColor: "text-decoration-color",
}
if cssTag, ok := colorTags[tag]; ok {
if color, ok := colorProperty(view, tag, session); ok {
updateCSSProperty(htmlID, cssTag, color.cssString(), session)
} else {
updateCSSProperty(htmlID, cssTag, "", session)
}
return
}
if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" {
n, _ := enumProperty(view, tag, session, 0)
updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n], session)
return
}
for _, floatTag := range []string{ScaleX, ScaleY, ScaleZ, RotateX, RotateY, RotateZ} {
if tag == floatTag {
if f, ok := floatProperty(view, floatTag, session, 0); ok {
updateCSSProperty(htmlID, floatTag, strconv.FormatFloat(f, 'g', -1, 64), session)
}
return
}
}
}
func (view *viewData) Get(tag string) interface{} {
return view.get(strings.ToLower(tag))
}
func (view *viewData) get(tag string) interface{} {
return view.viewStyle.get(tag)
}
func (view *viewData) htmlTag() string {
if semantics := GetSemantics(view, ""); semantics > DefaultSemantics {
values := enumProperties[Semantics].cssValues
if semantics < len(values) {
return values[semantics]
}
}
return "div"
}
func (view *viewData) closeHTMLTag() bool {
return true
}
func (view *viewData) htmlID() string {
if view._htmlID == "" {
view._htmlID = view.session.nextViewID()
}
return view._htmlID
}
func (view *viewData) htmlSubviews(self View, buffer *strings.Builder) {
}
func (view *viewData) addToCSSStyle(addCSS map[string]string) {
view.addCSS = addCSS
}
func (view *viewData) cssStyle(self View, builder cssBuilder) {
view.viewStyle.cssViewStyle(builder, view.session, self)
switch GetVisibility(view, "") {
case Invisible:
builder.add(`visibility`, `hidden`)
case Gone:
builder.add(`display`, `none`)
}
if view.addCSS != nil {
for tag, value := range view.addCSS {
builder.add(tag, value)
}
}
}
func (view *viewData) htmlProperties(self View, buffer *strings.Builder) {
view.created = true
if view.frame.Left != 0 || view.frame.Top != 0 || view.frame.Width != 0 || view.frame.Height != 0 {
buffer.WriteString(fmt.Sprintf(` data-left="%g" data-top="%g" data-width="%g" data-height="%g"`,
view.frame.Left, view.frame.Top, view.frame.Width, view.frame.Height))
}
}
func (view *viewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` data-disabled="1"`)
} else {
buffer.WriteString(` data-disabled="0"`)
}
}
func viewHTML(view View, buffer *strings.Builder) {
viewHTMLTag := view.htmlTag()
buffer.WriteRune('<')
buffer.WriteString(viewHTMLTag)
buffer.WriteString(` id="`)
buffer.WriteString(view.htmlID())
buffer.WriteRune('"')
disabled := IsDisabled(view)
if cls := view.htmlClass(disabled); cls != "" {
buffer.WriteString(` class="`)
buffer.WriteString(cls)
buffer.WriteRune('"')
}
var cssBuilder viewCSSBuilder
view.cssStyle(view, &cssBuilder)
if style := cssBuilder.finish(); style != "" {
buffer.WriteString(` style="`)
buffer.WriteString(style)
buffer.WriteRune('"')
}
buffer.WriteRune(' ')
view.htmlProperties(view, buffer)
buffer.WriteRune(' ')
view.htmlDisabledProperties(view, buffer)
if view.isNoResizeEvent() {
buffer.WriteString(` data-noresize="1" `)
} else {
buffer.WriteRune(' ')
}
if view.Focusable() && !disabled {
buffer.WriteString(`tabindex="0" `)
}
buffer.WriteString(`onscroll="scrollEvent(this, event)" `)
keyEventsHtml(view, buffer)
mouseEventsHtml(view, buffer)
pointerEventsHtml(view, buffer)
touchEventsHtml(view, buffer)
focusEventsHtml(view, buffer)
buffer.WriteRune('>')
view.htmlSubviews(view, buffer)
if view.closeHTMLTag() {
buffer.WriteString(`</`)
buffer.WriteString(viewHTMLTag)
buffer.WriteRune('>')
}
}
func (view *viewData) htmlClass(disabled bool) string {
cls := "ruiView"
disabledStyle := false
if disabled {
if value, ok := stringProperty(view, StyleDisabled, view.Session()); ok && value != "" {
cls += " " + value
disabledStyle = true
}
}
if !disabledStyle {
if value, ok := stringProperty(view, Style, view.Session()); ok {
cls += " " + value
}
}
if view.systemClass != "" {
cls = view.systemClass + " " + cls
}
return cls
}
func (view *viewData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case KeyDownEvent, KeyUpEvent:
if !IsDisabled(self) {
handleKeyEvents(self, command, data)
}
case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent:
handleMouseEvents(self, command, data)
case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel:
handlePointerEvents(self, command, data)
case TouchStart, TouchEnd, TouchMove, TouchCancel:
handleTouchEvents(self, command, data)
case FocusEvent, LostFocusEvent:
for _, listener := range getFocusListeners(view, "", command) {
listener(self)
}
case "scroll":
view.onScroll(view, dataFloatProperty(data, "x"), dataFloatProperty(data, "y"), dataFloatProperty(data, "width"), dataFloatProperty(data, "height"))
case "widthChanged":
if value, ok := data.PropertyValue("width"); ok {
if width, ok := StringToSizeUnit(value); ok {
self.setRaw(Width, width)
}
}
case "heightChanged":
if value, ok := data.PropertyValue("height"); ok {
if height, ok := StringToSizeUnit(value); ok {
self.setRaw(Height, height)
}
}
case "transitionEnd":
if property, ok := data.PropertyValue("property"); ok {
if animation, ok := view.animation[property]; ok {
delete(view.animation, property)
view.updateTransitionCSS()
if animation.FinishListener != nil {
animation.FinishListener.OnAnimationFinished(self, property)
}
}
return true
}
/*
case "resize":
floatProperty := func(tag string) float64 {
if value, ok := data.PropertyValue(tag); ok {
if result, err := strconv.ParseFloat(value, 64); err == nil {
return result
}
}
return 0
}
self.onResize(self, floatProperty("x"), floatProperty("y"), floatProperty("width"), floatProperty("height"))
return true
*/
default:
return false
}
return true
}
func ruiViewString(view View, viewTag string, writer ruiWriter) {
writer.startObject(viewTag)
tags := view.AllTags()
count := len(tags)
if count > 0 {
if count > 1 {
tagToStart := func(tag string) {
for i, t := range tags {
if t == tag {
if i > 0 {
for n := i; n > 0; n-- {
tags[n] = tags[n-1]
}
tags[0] = tag
}
return
}
}
}
tagToStart(StyleDisabled)
tagToStart(Style)
tagToStart(ID)
}
for _, tag := range tags {
if value := view.Get(tag); value != nil {
writer.writeProperty(tag, value)
}
}
}
writer.endObject()
}
func (view *viewData) ruiString(writer ruiWriter) {
ruiViewString(view, view.Tag(), writer)
}
func (view *viewData) String() string {
writer := newRUIWriter()
view.ruiString(writer)
return writer.finish()
}
// IsDisabled returns "true" if the view is disabled
func IsDisabled(view View) bool {
if disabled, _ := boolProperty(view, Disabled, view.Session()); disabled {
return true
}
if parent := view.Parent(); parent != nil {
return IsDisabled(parent)
}
return false
}

170
viewAnimation.go Normal file
View File

@ -0,0 +1,170 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
const (
// EaseTiming - a timing function which increases in velocity towards the middle of the transition, slowing back down at the end
EaseTiming = "ease"
// EaseInTiming - a timing function which starts off slowly, with the transition speed increasing until complete
EaseInTiming = "ease-in"
// EaseOutTiming - a timing function which starts transitioning quickly, slowing down the transition continues.
EaseOutTiming = "ease-out"
// EaseInOutTiming - a timing function which starts transitioning slowly, speeds up, and then slows down again.
EaseInOutTiming = "ease-in-out"
// LinearTiming - a timing function at an even speed
LinearTiming = "linear"
)
// StepsTiming return a timing function along stepCount stops along the transition, diplaying each stop for equal lengths of time
func StepsTiming(stepCount int) string {
return "steps(" + strconv.Itoa(stepCount) + ")"
}
// CubicBezierTiming return a cubic-Bezier curve timing function. x1 and x2 must be in the range [0, 1].
func CubicBezierTiming(x1, y1, x2, y2 float64) string {
if x1 < 0 {
x1 = 0
} else if x1 > 1 {
x1 = 1
}
if x2 < 0 {
x2 = 0
} else if x2 > 1 {
x2 = 1
}
return fmt.Sprintf("cubic-bezier(%g, %g, %g, %g)", x1, y1, x2, y2)
}
// AnimationFinishedListener describes the end of an animation event handler
type AnimationFinishedListener interface {
// OnAnimationFinished is called when a property animation is finished
OnAnimationFinished(view View, property string)
}
type Animation struct {
// Duration defines the time in seconds an animation should take to complete
Duration float64
// TimingFunction defines how intermediate values are calculated for a property being affected
// by an animation effect. If the value is "" then the "ease" function is used
TimingFunction string
// Delay defines the duration in seconds to wait before starting a property's animation.
Delay float64
// FinishListener defines the end of an animation event handler
FinishListener AnimationFinishedListener
}
type animationFinishedFunc struct {
finishFunc func(View, string)
}
func (listener animationFinishedFunc) OnAnimationFinished(view View, property string) {
if listener.finishFunc != nil {
listener.finishFunc(view, property)
}
}
func AnimationFinishedFunc(finishFunc func(View, string)) AnimationFinishedListener {
listener := new(animationFinishedFunc)
listener.finishFunc = finishFunc
return listener
}
func validateTimingFunction(timingFunction string) bool {
switch timingFunction {
case "", EaseTiming, EaseInTiming, EaseOutTiming, EaseInOutTiming, LinearTiming:
return true
}
size := len(timingFunction)
if size > 0 && timingFunction[size-1] == ')' {
if index := strings.IndexRune(timingFunction, '('); index > 0 {
args := timingFunction[index+1 : size-1]
switch timingFunction[:index] {
case "steps":
if _, err := strconv.Atoi(strings.Trim(args, " \t\n")); err == nil {
return true
}
case "cubic-bezier":
if params := strings.Split(args, ","); len(params) == 4 {
for _, param := range params {
if _, err := strconv.ParseFloat(strings.Trim(param, " \t\n"), 64); err != nil {
return false
}
}
return true
}
}
}
}
return false
}
func (view *viewData) SetAnimated(tag string, value interface{}, animation Animation) bool {
timingFunction, ok := view.session.resolveConstants(animation.TimingFunction)
if !ok || animation.Duration <= 0 || !validateTimingFunction(timingFunction) {
if view.Set(tag, value) {
if animation.FinishListener != nil {
animation.FinishListener.OnAnimationFinished(view, tag)
}
return true
}
return false
}
updateProperty(view.htmlID(), "ontransitionend", "transitionEndEvent(this, event)", view.session)
updateProperty(view.htmlID(), "ontransitioncancel", "transitionCancelEvent(this, event)", view.session)
animation.TimingFunction = timingFunction
view.animation[tag] = animation
view.updateTransitionCSS()
result := view.Set(tag, value)
if !result {
delete(view.animation, tag)
view.updateTransitionCSS()
}
return result
}
func (view *viewData) transitionCSS() string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for tag, animation := range view.animation {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(fmt.Sprintf(" %gs", animation.Duration))
if animation.TimingFunction != "" {
buffer.WriteRune(' ')
buffer.WriteString(animation.TimingFunction)
}
if animation.Delay > 0 {
if animation.TimingFunction == "" {
buffer.WriteString(" ease")
}
buffer.WriteString(fmt.Sprintf(" %gs", animation.Delay))
}
}
return buffer.String()
}
func (view *viewData) updateTransitionCSS() {
updateCSSProperty(view.htmlID(), "transition", view.transitionCSS(), view.Session())
}
// SetAnimated sets the property with name "tag" of the "rootView" subview with "viewID" id by value. Result:
// true - success,
// false - error (incompatible type or invalid format of a string value, see AppLog).
func SetAnimated(rootView View, viewID, tag string, value interface{}, animation Animation) bool {
if view := ViewByID(rootView, viewID); view != nil {
return view.SetAnimated(tag, value, animation)
}
return false
}

266
viewByID.go Normal file
View File

@ -0,0 +1,266 @@
package rui
// ViewByID return a View with id equal to the argument of the function or nil if there is no such View
func ViewByID(rootView View, id string) View {
if rootView == nil {
ErrorLog(`ViewByID(nil, "` + id + `"): rootView is nil`)
return nil
}
if rootView.ID() == id {
return rootView
}
if container, ok := rootView.(ParanetView); ok {
if view := viewByID(container, id); view != nil {
return view
}
}
ErrorLog(`ViewByID(_, "` + id + `"): View not found`)
return nil
}
func viewByID(rootView ParanetView, id string) View {
for _, view := range rootView.Views() {
if view != nil {
if view.ID() == id {
return view
}
if container, ok := view.(ParanetView); ok {
if v := viewByID(container, id); v != nil {
return v
}
}
}
}
return nil
}
// ViewsContainerByID return a ViewsContainer with id equal to the argument of the function or
// nil if there is no such View or View is not ViewsContainer
func ViewsContainerByID(rootView View, id string) ViewsContainer {
if view := ViewByID(rootView, id); view != nil {
if list, ok := view.(ViewsContainer); ok {
return list
}
ErrorLog(`ViewsContainerByID(_, "` + id + `"): The found View is not ViewsContainer`)
}
return nil
}
// ListLayoutByID return a ListLayout with id equal to the argument of the function or
// nil if there is no such View or View is not ListLayout
func ListLayoutByID(rootView View, id string) ListLayout {
if view := ViewByID(rootView, id); view != nil {
if list, ok := view.(ListLayout); ok {
return list
}
ErrorLog(`ListLayoutByID(_, "` + id + `"): The found View is not ListLayout`)
}
return nil
}
// StackLayoutByID return a StackLayout with id equal to the argument of the function or
// nil if there is no such View or View is not StackLayout
func StackLayoutByID(rootView View, id string) StackLayout {
if view := ViewByID(rootView, id); view != nil {
if list, ok := view.(StackLayout); ok {
return list
}
ErrorLog(`StackLayoutByID(_, "` + id + `"): The found View is not StackLayout`)
}
return nil
}
// GridLayoutByID return a GridLayout with id equal to the argument of the function or
// nil if there is no such View or View is not GridLayout
func GridLayoutByID(rootView View, id string) GridLayout {
if view := ViewByID(rootView, id); view != nil {
if list, ok := view.(GridLayout); ok {
return list
}
ErrorLog(`GridLayoutByID(_, "` + id + `"): The found View is not GridLayout`)
}
return nil
}
// ColumnLayoutByID return a ColumnLayout with id equal to the argument of the function or
// nil if there is no such View or View is not ColumnLayout
func ColumnLayoutByID(rootView View, id string) ColumnLayout {
if view := ViewByID(rootView, id); view != nil {
if list, ok := view.(ColumnLayout); ok {
return list
}
ErrorLog(`ColumnLayoutByID(_, "` + id + `"): The found View is not ColumnLayout`)
}
return nil
}
// DetailsViewByID return a ColumnLayout with id equal to the argument of the function or
// nil if there is no such View or View is not DetailsView
func DetailsViewByID(rootView View, id string) DetailsView {
if view := ViewByID(rootView, id); view != nil {
if details, ok := view.(DetailsView); ok {
return details
}
ErrorLog(`DetailsViewByID(_, "` + id + `"): The found View is not DetailsView`)
}
return nil
}
// DropDownListByID return a DropDownListView with id equal to the argument of the function or
// nil if there is no such View or View is not DropDownListView
func DropDownListByID(rootView View, id string) DropDownList {
if view := ViewByID(rootView, id); view != nil {
if list, ok := view.(DropDownList); ok {
return list
}
ErrorLog(`DropDownListByID(_, "` + id + `"): The found View is not DropDownList`)
}
return nil
}
// TabsLayoutByID return a TabsLayout with id equal to the argument of the function or
// nil if there is no such View or View is not TabsLayout
func TabsLayoutByID(rootView View, id string) TabsLayout {
if view := ViewByID(rootView, id); view != nil {
if list, ok := view.(TabsLayout); ok {
return list
}
ErrorLog(`TabsLayoutByID(_, "` + id + `"): The found View is not TabsLayout`)
}
return nil
}
// ListViewByID return a ListView with id equal to the argument of the function or
// nil if there is no such View or View is not ListView
func ListViewByID(rootView View, id string) ListView {
if view := ViewByID(rootView, id); view != nil {
if list, ok := view.(ListView); ok {
return list
}
ErrorLog(`ListViewByID(_, "` + id + `"): The found View is not ListView`)
}
return nil
}
// TextViewByID return a TextView with id equal to the argument of the function or
// nil if there is no such View or View is not TextView
func TextViewByID(rootView View, id string) TextView {
if view := ViewByID(rootView, id); view != nil {
if text, ok := view.(TextView); ok {
return text
}
ErrorLog(`TextViewByID(_, "` + id + `"): The found View is not TextView`)
}
return nil
}
// ButtonByID return a Button with id equal to the argument of the function or
// nil if there is no such View or View is not Button
func ButtonByID(rootView View, id string) Button {
if view := ViewByID(rootView, id); view != nil {
if button, ok := view.(Button); ok {
return button
}
ErrorLog(`ButtonByID(_, "` + id + `"): The found View is not Button`)
}
return nil
}
// CheckboxByID return a Checkbox with id equal to the argument of the function or
// nil if there is no such View or View is not Checkbox
func CheckboxByID(rootView View, id string) Checkbox {
if view := ViewByID(rootView, id); view != nil {
if checkbox, ok := view.(Checkbox); ok {
return checkbox
}
ErrorLog(`CheckboxByID(_, "` + id + `"): The found View is not Checkbox`)
}
return nil
}
// EditViewByID return a EditView with id equal to the argument of the function or
// nil if there is no such View or View is not EditView
func EditViewByID(rootView View, id string) EditView {
if view := ViewByID(rootView, id); view != nil {
if buttons, ok := view.(EditView); ok {
return buttons
}
ErrorLog(`EditViewByID(_, "` + id + `"): The found View is not EditView`)
}
return nil
}
// ProgressBarByID return a ProgressBar with id equal to the argument of the function or
// nil if there is no such View or View is not ProgressBar
func ProgressBarByID(rootView View, id string) ProgressBar {
if view := ViewByID(rootView, id); view != nil {
if buttons, ok := view.(ProgressBar); ok {
return buttons
}
ErrorLog(`ProgressBarByID(_, "` + id + `"): The found View is not ProgressBar`)
}
return nil
}
// NumberPickerByID return a NumberPicker with id equal to the argument of the function or
// nil if there is no such View or View is not NumberPicker
func NumberPickerByID(rootView View, id string) NumberPicker {
if view := ViewByID(rootView, id); view != nil {
if input, ok := view.(NumberPicker); ok {
return input
}
ErrorLog(`NumberPickerByID(_, "` + id + `"): The found View is not NumberPicker`)
}
return nil
}
// CanvasViewByID return a CanvasView with id equal to the argument of the function or
// nil if there is no such View or View is not CanvasView
func CanvasViewByID(rootView View, id string) CanvasView {
if view := ViewByID(rootView, id); view != nil {
if canvas, ok := view.(CanvasView); ok {
return canvas
}
ErrorLog(`CanvasViewByID(_, "` + id + `"): The found View is not CanvasView`)
}
return nil
}
/*
// TableViewByID return a TableView with id equal to the argument of the function or
// nil if there is no such View or View is not TableView
func TableViewByID(rootView View, id string) TableView {
if view := ViewByID(rootView, id); view != nil {
if canvas, ok := view.(TableView); ok {
return canvas
}
ErrorLog(`TableViewByID(_, "` + id + `"): The found View is not TableView`)
}
return nil
}
*/
// AudioPlayerByID return a AudioPlayer with id equal to the argument of the function or
// nil if there is no such View or View is not AudioPlayer
func AudioPlayerByID(rootView View, id string) AudioPlayer {
if view := ViewByID(rootView, id); view != nil {
if canvas, ok := view.(AudioPlayer); ok {
return canvas
}
ErrorLog(`AudioPlayerByID(_, "` + id + `"): The found View is not AudioPlayer`)
}
return nil
}
// VideoPlayerByID return a VideoPlayer with id equal to the argument of the function or
// nil if there is no such View or View is not VideoPlayer
func VideoPlayerByID(rootView View, id string) VideoPlayer {
if view := ViewByID(rootView, id); view != nil {
if canvas, ok := view.(VideoPlayer); ok {
return canvas
}
ErrorLog(`VideoPlayerByID(_, "` + id + `"): The found View is not VideoPlayer`)
}
return nil
}

594
viewClip.go Normal file
View File

@ -0,0 +1,594 @@
package rui
import (
"fmt"
"strings"
)
// ClipShape defines a View clipping area
type ClipShape interface {
Properties
fmt.Stringer
ruiStringer
cssStyle(session Session) string
valid(session Session) bool
}
type insetClip struct {
propertyList
}
type ellipseClip struct {
propertyList
}
type polygonClip struct {
points []interface{}
}
// InsetClip creates a rectangle View clipping area.
// top - offset from the top border of a View;
// right - offset from the right border of a View;
// bottom - offset from the bottom border of a View;
// left - offset from the left border of a View;
// radius - corner radius, pass nil if you don't need to round corners
func InsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShape {
clip := new(insetClip)
clip.init()
clip.Set(Top, top)
clip.Set(Right, right)
clip.Set(Bottom, bottom)
clip.Set(Left, left)
if radius != nil {
clip.Set(Radius, radius)
}
return clip
}
// CircleClip creates a circle View clipping area.
func CircleClip(x, y, radius SizeUnit) ClipShape {
clip := new(ellipseClip)
clip.init()
clip.Set(X, x)
clip.Set(Y, y)
clip.Set(Radius, radius)
return clip
}
// EllipseClip creates a ellipse View clipping area.
func EllipseClip(x, y, rx, ry SizeUnit) ClipShape {
clip := new(ellipseClip)
clip.init()
clip.Set(X, x)
clip.Set(Y, y)
clip.Set(RadiusX, rx)
clip.Set(RadiusY, ry)
return clip
}
// PolygonClip creates a polygon View clipping area.
// The elements of the function argument can be or text constants,
// or the text representation of SizeUnit, or elements of SizeUnit type.
func PolygonClip(points []interface{}) ClipShape {
clip := new(polygonClip)
clip.points = []interface{}{}
if clip.Set(Points, points) {
return clip
}
return nil
}
// PolygonPointsClip creates a polygon View clipping area.
func PolygonPointsClip(points []SizeUnit) ClipShape {
clip := new(polygonClip)
clip.points = []interface{}{}
if clip.Set(Points, points) {
return clip
}
return nil
}
func (clip *insetClip) Set(tag string, value interface{}) bool {
switch strings.ToLower(tag) {
case Top, Right, Bottom, Left:
if value == nil {
clip.Remove(tag)
return true
}
return clip.setSizeProperty(tag, value)
case Radius:
return clip.setRadius(value)
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
return clip.setRadiusElement(tag, value)
}
ErrorLogF(`"%s" property is not supported by the inset clip shape`, tag)
return false
}
func (clip *insetClip) String() string {
writer := newRUIWriter()
clip.ruiString(writer)
return writer.finish()
}
func (clip *insetClip) ruiString(writer ruiWriter) {
writer.startObject("inset")
for _, tag := range []string{Top, Right, Bottom, Left} {
if value, ok := clip.properties[tag]; ok {
switch value := value.(type) {
case string:
writer.writeProperty(tag, value)
case fmt.Stringer:
writer.writeProperty(tag, value.String())
}
}
}
if value := clip.Get(Radius); value != nil {
switch value := value.(type) {
case RadiusProperty:
writer.writeProperty(Radius, value.String())
case SizeUnit:
writer.writeProperty(Radius, value.String())
case string:
writer.writeProperty(Radius, value)
}
}
writer.endObject()
}
func (clip *insetClip) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
leadText := "inset("
for _, tag := range []string{Top, Right, Bottom, Left} {
value, _ := sizeProperty(clip, tag, session)
buffer.WriteString(leadText)
buffer.WriteString(value.cssString("0px"))
leadText = " "
}
if radius := getRadiusProperty(clip); radius != nil {
buffer.WriteString(" round ")
buffer.WriteString(radius.BoxRadius(session).cssString())
}
buffer.WriteRune(')')
return buffer.String()
}
func (clip *insetClip) valid(session Session) bool {
for _, tag := range []string{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
if value, ok := sizeProperty(clip, tag, session); ok && value.Type != Auto && value.Value != 0 {
return true
}
}
return false
}
func (clip *ellipseClip) Set(tag string, value interface{}) bool {
if value == nil {
clip.Remove(tag)
}
switch strings.ToLower(tag) {
case X, Y:
return clip.setSizeProperty(tag, value)
case Radius:
result := clip.setSizeProperty(tag, value)
if result {
delete(clip.properties, RadiusX)
delete(clip.properties, RadiusY)
}
return result
case RadiusX:
result := clip.setSizeProperty(tag, value)
if result {
if r, ok := clip.properties[Radius]; ok {
clip.properties[RadiusY] = r
delete(clip.properties, Radius)
}
}
return result
case RadiusY:
result := clip.setSizeProperty(tag, value)
if result {
if r, ok := clip.properties[Radius]; ok {
clip.properties[RadiusX] = r
delete(clip.properties, Radius)
}
}
return result
}
ErrorLogF(`"%s" property is not supported by the inset clip shape`, tag)
return false
}
func (clip *ellipseClip) String() string {
writer := newRUIWriter()
clip.ruiString(writer)
return writer.finish()
}
func (clip *ellipseClip) ruiString(writer ruiWriter) {
writeProperty := func(tag string, value interface{}) {
switch value := value.(type) {
case string:
writer.writeProperty(tag, value)
case fmt.Stringer:
writer.writeProperty(tag, value.String())
}
}
if r, ok := clip.properties[Radius]; ok {
writer.startObject("circle")
writeProperty(Radius, r)
} else {
writer.startObject("ellipse")
for _, tag := range []string{RadiusX, RadiusY} {
if value, ok := clip.properties[tag]; ok {
writeProperty(tag, value)
}
}
}
for _, tag := range []string{X, Y} {
if value, ok := clip.properties[tag]; ok {
writeProperty(tag, value)
}
}
writer.endObject()
}
func (clip *ellipseClip) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if r, ok := sizeProperty(clip, Radius, session); ok {
buffer.WriteString("circle(")
buffer.WriteString(r.cssString("0"))
} else {
rx, _ := sizeProperty(clip, RadiusX, session)
ry, _ := sizeProperty(clip, RadiusX, session)
buffer.WriteString("ellipse(")
buffer.WriteString(rx.cssString("0"))
buffer.WriteRune(' ')
buffer.WriteString(ry.cssString("0"))
}
buffer.WriteString(" at ")
x, _ := sizeProperty(clip, X, session)
buffer.WriteString(x.cssString("0"))
buffer.WriteRune(' ')
y, _ := sizeProperty(clip, Y, session)
buffer.WriteString(y.cssString("0"))
buffer.WriteRune(')')
return buffer.String()
}
func (clip *ellipseClip) valid(session Session) bool {
if value, ok := sizeProperty(clip, Radius, session); ok && value.Type != Auto && value.Value != 0 {
return true
}
rx, okX := sizeProperty(clip, RadiusX, session)
ry, okY := sizeProperty(clip, RadiusY, session)
return okX && okY && rx.Type != Auto && rx.Value != 0 && ry.Type != Auto && ry.Value != 0
}
func (clip *polygonClip) Get(tag string) interface{} {
if Points == strings.ToLower(tag) {
return clip.points
}
return nil
}
func (clip *polygonClip) getRaw(tag string) interface{} {
return clip.Get(tag)
}
func (clip *polygonClip) Set(tag string, value interface{}) bool {
if Points == strings.ToLower(tag) {
switch value := value.(type) {
case []interface{}:
result := true
clip.points = make([]interface{}, len(value))
for i, val := range value {
switch val := val.(type) {
case string:
if isConstantName(val) {
clip.points[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
clip.points[i] = size
} else {
notCompatibleType(tag, val)
result = false
}
case SizeUnit:
clip.points[i] = val
default:
notCompatibleType(tag, val)
clip.points[i] = AutoSize()
result = false
}
}
return result
case []SizeUnit:
clip.points = make([]interface{}, len(value))
for i, point := range value {
clip.points[i] = point
}
return true
case string:
result := true
values := strings.Split(value, ",")
clip.points = make([]interface{}, len(values))
for i, val := range values {
val = strings.Trim(val, " \t\n\r")
if isConstantName(val) {
clip.points[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
clip.points[i] = size
} else {
notCompatibleType(tag, val)
result = false
}
}
return result
}
}
return false
}
func (clip *polygonClip) setRaw(tag string, value interface{}) {
clip.Set(tag, value)
}
func (clip *polygonClip) Remove(tag string) {
if Points == strings.ToLower(tag) {
clip.points = []interface{}{}
}
}
func (clip *polygonClip) Clear() {
clip.points = []interface{}{}
}
func (clip *polygonClip) AllTags() []string {
return []string{Points}
}
func (clip *polygonClip) String() string {
writer := newRUIWriter()
clip.ruiString(writer)
return writer.finish()
}
func (clip *polygonClip) ruiString(writer ruiWriter) {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writer.startObject("polygon")
if clip.points != nil {
for i, value := range clip.points {
if i > 0 {
buffer.WriteString(", ")
}
switch value := value.(type) {
case string:
buffer.WriteString(value)
case fmt.Stringer:
buffer.WriteString(value.String())
default:
buffer.WriteString("0px")
}
}
writer.writeProperty(Points, buffer.String())
}
writer.endObject()
}
func (clip *polygonClip) cssStyle(session Session) string {
if clip.points == nil {
return ""
}
count := len(clip.points)
if count < 2 {
return ""
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writePoint := func(value interface{}) {
switch value := value.(type) {
case string:
if val, ok := session.resolveConstants(value); ok {
if size, ok := StringToSizeUnit(val); ok {
buffer.WriteString(size.cssString("0px"))
return
}
}
case SizeUnit:
buffer.WriteString(value.cssString("0px"))
return
}
buffer.WriteString("0px")
}
leadText := "polygon("
for i := 1; i < count; i += 2 {
buffer.WriteString(leadText)
writePoint(clip.points[i-1])
buffer.WriteRune(' ')
writePoint(clip.points[i])
leadText = ", "
}
buffer.WriteRune(')')
return buffer.String()
}
func (clip *polygonClip) valid(session Session) bool {
if clip.points == nil || len(clip.points) == 0 {
return false
}
return true
}
func parseClipShape(obj DataObject) ClipShape {
switch obj.Tag() {
case "inset":
clip := new(insetClip)
for _, tag := range []string{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
if value, ok := obj.PropertyValue(tag); ok {
clip.Set(tag, value)
}
}
return clip
case "circle":
clip := new(ellipseClip)
for _, tag := range []string{X, Y, Radius} {
if value, ok := obj.PropertyValue(tag); ok {
clip.Set(tag, value)
}
}
return clip
case "ellipse":
clip := new(ellipseClip)
for _, tag := range []string{X, Y, RadiusX, RadiusY} {
if value, ok := obj.PropertyValue(tag); ok {
clip.Set(tag, value)
}
}
return clip
case "polygon":
clip := new(ellipseClip)
if value, ok := obj.PropertyValue(Points); ok {
clip.Set(Points, value)
}
return clip
}
return nil
}
func (style *viewStyle) setClipShape(tag string, value interface{}) bool {
switch value := value.(type) {
case ClipShape:
style.properties[tag] = value
return true
case string:
if isConstantName(value) {
style.properties[tag] = value
return true
}
if obj := NewDataObject(value); obj == nil {
if clip := parseClipShape(obj); clip != nil {
style.properties[tag] = clip
return true
}
}
case DataObject:
if clip := parseClipShape(value); clip != nil {
style.properties[tag] = clip
return true
}
case DataValue:
if value.IsObject() {
if clip := parseClipShape(value.Object()); clip != nil {
style.properties[tag] = clip
return true
}
}
}
notCompatibleType(tag, value)
return false
}
func getClipShape(prop Properties, tag string, session Session) ClipShape {
if value := prop.getRaw(tag); value != nil {
switch value := value.(type) {
case ClipShape:
return value
case string:
if text, ok := session.resolveConstants(value); ok {
if obj := NewDataObject(text); obj == nil {
return parseClipShape(obj)
}
}
}
}
return nil
}
// GetClip returns a View clipping area.
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
func GetClip(view View, subviewID string) ClipShape {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
return getClipShape(view, Clip, view.Session())
}
return nil
}
// GetShapeOutside returns a shape around which adjacent inline content.
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
func GetShapeOutside(view View, subviewID string) ClipShape {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
return getClipShape(view, ShapeOutside, view.Session())
}
return nil
}

138
viewFactory.go Normal file
View File

@ -0,0 +1,138 @@
package rui
import (
"path/filepath"
"strings"
)
var viewCreators = map[string]func(Session) View{
"View": newView,
"ColumnLayout": newColumnLayout,
"ListLayout": newListLayout,
"GridLayout": newGridLayout,
"StackLayout": newStackLayout,
"TabsLayout": newTabsLayout,
"AbsoluteLayout": newAbsoluteLayout,
"Resizable": newResizable,
"DetailsView": newDetailsView,
"TextView": newTextView,
"Button": newButton,
"Checkbox": newCheckbox,
"DropDownList": newDropDownList,
"ProgressBar": newProgressBar,
"NumberPicker": newNumberPicker,
"ColorPicker": newColorPicker,
"DatePicker": newDatePicker,
"TimePicker": newTimePicker,
"EditView": newEditView,
"ListView": newListView,
"CanvasView": newCanvasView,
"ImageView": newImageView,
"TableView": newTableView,
"AudioPlayer": newAudioPlayer,
"VideoPlayer": newVideoPlayer,
}
// RegisterViewCreator register function of creating view
func RegisterViewCreator(tag string, creator func(Session) View) bool {
builtinViews := []string{
"View",
"ViewsContainer",
"ColumnLayout",
"ListLayout",
"GridLayout",
"StackLayout",
"TabsLayout",
"AbsoluteLayout",
"Resizable",
"DetailsView",
"TextView",
"Button",
"Checkbox",
"DropDownList",
"ProgressBar",
"NumberPicker",
"ColorPicker",
"DatePicker",
"TimePicker",
"EditView",
"ListView",
"CanvasView",
"ImageView",
"TableView",
}
for _, name := range builtinViews {
if name == tag {
return false
}
}
viewCreators[tag] = creator
return true
}
// CreateViewFromObject create new View and initialize it by Node data
func CreateViewFromObject(session Session, object DataObject) View {
tag := object.Tag()
if creator, ok := viewCreators[tag]; ok {
if !session.ignoreViewUpdates() {
session.setIgnoreViewUpdates(true)
defer session.setIgnoreViewUpdates(false)
}
view := creator(session)
if customView, ok := view.(CustomView); ok {
if !InitCustomView(customView, tag, session, nil) {
return nil
}
}
parseProperties(view, object)
return view
}
ErrorLog(`Unknown view type "` + object.Tag() + `"`)
return nil
}
// CreateViewFromText create new View and initialize it by content of text
func CreateViewFromText(session Session, text string) View {
if data := ParseDataText(text); data != nil {
return CreateViewFromObject(session, data)
}
return nil
}
// CreateViewFromResources create new View and initialize it by the content of
// the resource file from "views" directory
func CreateViewFromResources(session Session, name string) View {
if strings.ToLower(filepath.Ext(name)) != ".rui" {
name += ".rui"
}
for _, fs := range resources.embedFS {
rootDirs := embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, rawDir:
// do nothing
case viewDir:
if data, err := fs.ReadFile(dir + "/" + name); err == nil {
if data := ParseDataText(string(data)); data != nil {
return CreateViewFromObject(session, data)
}
}
default:
if data, err := fs.ReadFile(dir + "/" + viewDir + "/" + name); err == nil {
if data := ParseDataText(string(data)); data != nil {
return CreateViewFromObject(session, data)
}
}
}
}
}
return nil
}

264
viewFilter.go Normal file
View File

@ -0,0 +1,264 @@
package rui
import (
"fmt"
"strings"
)
const (
// Blur is the constant for the "blur" property tag of the ViewFilter interface.
// The "blur" float64 property applies a Gaussian blur. The value of radius defines the value
// of the standard deviation to the Gaussian function, or how many pixels on the screen blend
// into each other, so a larger value will create more blur. The lacuna value for interpolation is 0.
// The parameter is specified as a length in pixels.
Blur = "blur"
// Brightness is the constant for the "brightness" property tag of the ViewFilter interface.
// The "brightness" float64 property applies a linear multiplier to input image, making it appear more
// or less bright. A value of 0% will create an image that is completely black.
// A value of 100% leaves the input unchanged. Other values are linear multipliers on the effect.
// Values of an amount over 100% are allowed, providing brighter results.
Brightness = "brightness"
// Contrast is the constant for the "contrast" property tag of the ViewFilter interface.
// The "contrast" float64 property adjusts the contrast of the input.
// A value of 0% will create an image that is completely black. A value of 100% leaves the input unchanged.
// Values of amount over 100% are allowed, providing results with less contrast.
Contrast = "contrast"
// DropShadow is the constant for the "drop-shadow" property tag of the ViewFilter interface.
// The "drop-shadow" property applies a drop shadow effect to the input image.
// A drop shadow is effectively a blurred, offset version of the input image's alpha mask
// drawn in a particular color, composited below the image.
// Shadow parameters are set using the ViewShadow interface
DropShadow = "drop-shadow"
// Grayscale is the constant for the "grayscale" property tag of the ViewFilter interface.
// The "grayscale" float64 property converts the input image to grayscale.
// The value of amount defines the proportion of the conversion.
// A value of 100% is completely grayscale. A value of 0% leaves the input unchanged.
// Values between 0% and 100% are linear multipliers on the effect.
Grayscale = "grayscale"
// HueRotate is the constant for the "hue-rotate" property tag of the ViewFilter interface.
// The "hue-rotate" AngleUnit property applies a hue rotation on the input image.
// The value of angle defines the number of degrees around the color circle the input samples will be adjusted.
// A value of 0deg leaves the input unchanged. If the angle parameter is missing, a value of 0deg is used.
// Though there is no maximum value, the effect of values above 360deg wraps around.
HueRotate = "hue-rotate"
// Invert is the constant for the "invert" property tag of the ViewFilter interface.
// The "invert" float64 property inverts the samples in the input image.
// The value of amount defines the proportion of the conversion.
// A value of 100% is completely inverted. A value of 0% leaves the input unchanged.
// Values between 0% and 100% are linear multipliers on the effect.
Invert = "invert"
// Saturate is the constant for the "saturate" property tag of the ViewFilter interface.
// The "saturate" float64 property saturates the input image.
// The value of amount defines the proportion of the conversion.
// A value of 0% is completely un-saturated. A value of 100% leaves the input unchanged.
// Other values are linear multipliers on the effect.
// Values of amount over 100% are allowed, providing super-saturated results.
Saturate = "saturate"
// Sepia is the constant for the "sepia" property tag of the ViewFilter interface.
// The "sepia" float64 property converts the input image to sepia.
// The value of amount defines the proportion of the conversion.
// A value of 100% is completely sepia. A value of 0% leaves the input unchanged.
// Values between 0% and 100% are linear multipliers on the effect.
Sepia = "sepia"
//Opacity = "opacity"
)
// ViewFilter defines an applied to a View a graphical effects like blur or color shift.
// Allowable properties are Blur, Brightness, Contrast, DropShadow, Grayscale, HueRotate, Invert, Opacity, Saturate, and Sepia
type ViewFilter interface {
Properties
fmt.Stringer
ruiStringer
cssStyle(session Session) string
}
type viewFilter struct {
propertyList
}
// NewViewFilter creates the new ViewFilter
func NewViewFilter(params Params) ViewFilter {
filter := new(viewFilter)
filter.init()
for tag, value := range params {
filter.Set(tag, value)
}
if len(filter.properties) > 0 {
return filter
}
return nil
}
func newViewFilter(obj DataObject) ViewFilter {
filter := new(viewFilter)
filter.init()
for i := 0; i < obj.PropertyCount(); i++ {
if node := obj.Property(i); node != nil {
tag := node.Tag()
switch node.Type() {
case TextNode:
filter.Set(tag, node.Text())
case ObjectNode:
if tag == HueRotate {
// TODO
} else {
ErrorLog(`Invalid value of "` + tag + `"`)
}
default:
ErrorLog(`Invalid value of "` + tag + `"`)
}
}
}
if len(filter.properties) > 0 {
return filter
}
ErrorLog("Empty view filter")
return nil
}
func (filter *viewFilter) Set(tag string, value interface{}) bool {
if value == nil {
filter.Remove(tag)
return true
}
switch strings.ToLower(tag) {
case Blur, Brightness, Contrast, Saturate:
return filter.setFloatProperty(tag, value, 0, 10000)
case Grayscale, Invert, Opacity, Sepia:
return filter.setFloatProperty(tag, value, 0, 100)
case HueRotate:
return filter.setAngleProperty(tag, value)
case DropShadow:
return filter.setShadow(tag, value)
}
ErrorLogF(`"%s" property is not supported by the view filter`, tag)
return false
}
func (filter *viewFilter) String() string {
writer := newRUIWriter()
filter.ruiString(writer)
return writer.finish()
}
func (filter *viewFilter) ruiString(writer ruiWriter) {
writer.startObject("filter")
for tag, value := range filter.properties {
writer.writeProperty(tag, value)
}
writer.endObject()
}
func (filter *viewFilter) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if value, ok := floatProperty(filter, Blur, session, 0); ok {
size := SizeUnit{Type: SizeInPixel, Value: value}
buffer.WriteString(Blur)
buffer.WriteRune('(')
buffer.WriteString(size.cssString("0px"))
buffer.WriteRune(')')
}
for _, tag := range []string{Brightness, Contrast, Saturate, Grayscale, Invert, Opacity, Sepia} {
if value, ok := floatProperty(filter, tag, session, 0); ok {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(fmt.Sprintf("%s(%g%%)", tag, value))
}
}
if value, ok := angleProperty(filter, HueRotate, session); ok {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(HueRotate)
buffer.WriteRune('(')
buffer.WriteString(value.cssString())
buffer.WriteRune(')')
}
var lead string
if buffer.Len() > 0 {
lead = " drop-shadow("
} else {
lead = "drop-shadow("
}
for _, shadow := range getShadows(filter, DropShadow) {
if shadow.cssTextStyle(buffer, session, lead) {
buffer.WriteRune(')')
lead = " drop-shadow("
}
}
return buffer.String()
}
func (style *viewStyle) setFilter(value interface{}) bool {
switch value := value.(type) {
case ViewFilter:
style.properties[Filter] = value
return true
case string:
if obj := NewDataObject(value); obj == nil {
if filter := newViewFilter(obj); filter != nil {
style.properties[Filter] = filter
return true
}
}
case DataObject:
if filter := newViewFilter(value); filter != nil {
style.properties[Filter] = filter
return true
}
case DataValue:
if value.IsObject() {
if filter := newViewFilter(value.Object()); filter != nil {
style.properties[Filter] = filter
return true
}
}
}
notCompatibleType(Filter, value)
return false
}
// GetFilter returns a View graphical effects like blur or color shift.
// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned
func GetFilter(view View, subviewID string) ViewFilter {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.getRaw(Filter); value != nil {
if filter, ok := value.(ViewFilter); ok {
return filter
}
}
}
return nil
}

422
viewStyle.go Normal file
View File

@ -0,0 +1,422 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
// ViewStyle interface of the style of view
type ViewStyle interface {
Properties
cssViewStyle(buffer cssBuilder, session Session, view View)
}
type viewStyle struct {
propertyList
//transitions map[string]ViewTransition
}
// Range defines range limits. The First and Last value are included in the range
type Range struct {
First, Last int
}
// String returns a string representation of the Range struct
func (r Range) String() string {
if r.First == r.Last {
return fmt.Sprintf("%d", r.First)
}
return fmt.Sprintf("%d:%d", r.First, r.Last)
}
func (r *Range) setValue(value string) bool {
var err error
if strings.Contains(value, ":") {
values := strings.Split(value, ":")
if len(values) != 2 {
ErrorLog("Invalid range value: " + value)
return false
}
if r.First, err = strconv.Atoi(strings.Trim(values[0], " \t\n\r")); err != nil {
ErrorLog(`Invalid first range value "` + value + `" (` + err.Error() + ")")
return false
}
if r.Last, err = strconv.Atoi(strings.Trim(values[1], " \t\n\r")); err != nil {
ErrorLog(`Invalid last range value "` + value + `" (` + err.Error() + ")")
return false
}
return true
}
if r.First, err = strconv.Atoi(value); err != nil {
ErrorLog(`Invalid range value "` + value + `" (` + err.Error() + ")")
return false
}
r.Last = r.First
return true
}
func (style *viewStyle) init() {
style.propertyList.init()
//style.shadows = []ViewShadow{}
//style.transitions = map[string]ViewTransition{}
}
// NewViewStyle create new ViewStyle object
func NewViewStyle(params Params) ViewStyle {
style := new(viewStyle)
style.init()
for tag, value := range params {
style.Set(tag, value)
}
return style
}
func (style *viewStyle) cssTextDecoration(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
noDecoration := false
if strikethrough, ok := boolProperty(style, Strikethrough, session); ok {
if strikethrough {
buffer.WriteString("line-through")
}
noDecoration = true
}
if overline, ok := boolProperty(style, Overline, session); ok {
if overline {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString("overline")
}
noDecoration = true
}
if underline, ok := boolProperty(style, Underline, session); ok {
if underline {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString("underline")
}
noDecoration = true
}
if buffer.Len() == 0 && noDecoration {
return "none"
}
return buffer.String()
}
func split4Values(text string) []string {
values := strings.Split(text, ",")
count := len(values)
switch count {
case 1, 4:
return values
case 2:
if strings.Trim(values[1], " \t\r\n") == "" {
return values[:1]
}
case 5:
if strings.Trim(values[4], " \t\r\n") != "" {
return values[:4]
}
}
return []string{}
}
func (style *viewStyle) backgroundCSS(view View) string {
if value, ok := style.properties[Background]; ok {
if backgrounds, ok := value.([]BackgroundElement); ok {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, background := range backgrounds {
if value := background.cssStyle(view); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(value)
}
}
if buffer.Len() > 0 {
return buffer.String()
}
}
}
return ""
}
func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session, view View) {
if margin, ok := boundsProperty(style, Margin, session); ok {
margin.cssValue(Margin, builder)
}
if padding, ok := boundsProperty(style, Padding, session); ok {
padding.cssValue(Padding, builder)
}
if border := getBorder(style, Border); border != nil {
border.cssStyle(builder, session)
border.cssWidth(builder, session)
border.cssColor(builder, session)
}
radius := getRadius(style, session)
radius.cssValue(builder)
if outline := getOutline(style); outline != nil {
outline.ViewOutline(session).cssValue(builder)
}
if z, ok := intProperty(style, ZIndex, session, 0); ok {
builder.add(ZIndex, strconv.Itoa(z))
}
if opacity, ok := floatProperty(style, Opacity, session, 1.0); ok && opacity >= 0 && opacity <= 1 {
builder.add(Opacity, strconv.FormatFloat(opacity, 'f', 3, 32))
}
if n, ok := intProperty(style, ColumnCount, session, 0); ok && n > 0 {
builder.add(ColumnCount, strconv.Itoa(n))
}
for _, tag := range []string{
Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Left, Right, Top, Bottom,
TextSize, TextIndent, LetterSpacing, WordSpacing, LineHeight, TextLineThickness,
GridRowGap, GridColumnGap, ColumnGap, ColumnWidth} {
if size, ok := sizeProperty(style, tag, session); ok && size.Type != Auto {
cssTag, ok := sizeProperties[tag]
if !ok {
cssTag = tag
}
builder.add(cssTag, size.cssString(""))
}
}
colorProperties := []struct{ property, cssTag string }{
{BackgroundColor, BackgroundColor},
{TextColor, "color"},
{TextLineColor, "text-decoration-color"},
}
for _, p := range colorProperties {
if color, ok := colorProperty(style, p.property, session); ok && color != 0 {
builder.add(p.cssTag, color.cssString())
}
}
if value, ok := enumProperty(style, BackgroundClip, session, 0); ok {
builder.add(BackgroundClip, enumProperties[BackgroundClip].values[value])
}
if background := style.backgroundCSS(view); background != "" {
builder.add("background", background)
}
if font, ok := stringProperty(style, FontName, session); ok && font != "" {
builder.add(`font-family`, font)
}
writingMode := 0
for _, tag := range []string{
TextAlign, TextTransform, TextWeight, TextLineStyle, WritingMode, TextDirection,
VerticalTextOrientation, CellVerticalAlign, CellHorizontalAlign, Cursor, WhiteSpace,
WordBreak, TextOverflow, Float, TableVerticalAlign} {
if data, ok := enumProperties[tag]; ok {
if tag != VerticalTextOrientation || (writingMode != VerticalLeftToRight && writingMode != VerticalRightToLeft) {
if value, ok := enumProperty(style, tag, session, 0); ok {
cssValue := data.values[value]
if cssValue != "" {
builder.add(data.cssTag, cssValue)
}
if tag == WritingMode {
writingMode = value
}
}
}
}
}
for _, prop := range []struct{ tag, cssTag, off, on string }{
{tag: Italic, cssTag: "font-style", off: "normal", on: "italic"},
{tag: SmallCaps, cssTag: "font-variant", off: "normal", on: "small-caps"},
} {
if flag, ok := boolProperty(style, prop.tag, session); ok {
if flag {
builder.add(prop.cssTag, prop.on)
} else {
builder.add(prop.cssTag, prop.off)
}
}
}
if text := style.cssTextDecoration(session); text != "" {
builder.add("text-decoration", text)
}
if css := shadowCSS(style, Shadow, session); css != "" {
builder.add("box-shadow", css)
}
if css := shadowCSS(style, TextShadow, session); css != "" {
builder.add("text-shadow", css)
}
if value, ok := style.properties[ColumnSeparator]; ok {
if separator, ok := value.(ColumnSeparatorProperty); ok {
if css := separator.cssValue(session); css != "" {
builder.add("column-rule", css)
}
}
}
if avoid, ok := boolProperty(style, AvoidBreak, session); ok {
if avoid {
builder.add("break-inside", "avoid")
} else {
builder.add("break-inside", "auto")
}
}
wrap, _ := enumProperty(style, Wrap, session, 0)
orientation, ok := getOrientation(style, session)
if ok || wrap > 0 {
cssText := enumProperties[Orientation].cssValues[orientation]
switch wrap {
case WrapOn:
cssText += " wrap"
case WrapReverse:
cssText += " wrap-reverse"
}
builder.add(`flex-flow`, cssText)
}
rows := (orientation == StartToEndOrientation || orientation == EndToStartOrientation)
var hAlignTag, vAlignTag string
if rows {
hAlignTag = `justify-content`
vAlignTag = `align-items`
} else {
hAlignTag = `align-items`
vAlignTag = `justify-content`
}
if align, ok := enumProperty(style, HorizontalAlign, session, LeftAlign); ok {
switch align {
case LeftAlign:
if (!rows && wrap == WrapReverse) || orientation == EndToStartOrientation {
builder.add(hAlignTag, `flex-end`)
} else {
builder.add(hAlignTag, `flex-start`)
}
case RightAlign:
if (!rows && wrap == WrapReverse) || orientation == EndToStartOrientation {
builder.add(hAlignTag, `flex-start`)
} else {
builder.add(hAlignTag, `flex-end`)
}
case CenterAlign:
builder.add(hAlignTag, `center`)
case StretchAlign:
if rows {
builder.add(hAlignTag, `space-between`)
} else {
builder.add(hAlignTag, `stretch`)
}
}
}
if align, ok := enumProperty(style, VerticalAlign, session, LeftAlign); ok {
switch align {
case TopAlign:
if (rows && wrap == WrapReverse) || orientation == BottomUpOrientation {
builder.add(vAlignTag, `flex-end`)
} else {
builder.add(vAlignTag, `flex-start`)
}
case BottomAlign:
if (rows && wrap == WrapReverse) || orientation == BottomUpOrientation {
builder.add(vAlignTag, `flex-start`)
} else {
builder.add(vAlignTag, `flex-end`)
}
case CenterAlign:
builder.add(vAlignTag, `center`)
case StretchAlign:
if rows {
builder.add(hAlignTag, `stretch`)
} else {
builder.add(hAlignTag, `space-between`)
}
}
}
if r, ok := rangeProperty(style, Row, session); ok {
builder.add("grid-row-start", strconv.Itoa(r.First+1))
builder.add("grid-row-end", strconv.Itoa(r.Last+2))
}
if r, ok := rangeProperty(style, Column, session); ok {
builder.add("grid-column-start", strconv.Itoa(r.First+1))
builder.add("grid-column-end", strconv.Itoa(r.Last+2))
}
if text := style.gridCellSizesCSS(CellWidth, session); text != "" {
builder.add(`grid-template-columns`, text)
}
if text := style.gridCellSizesCSS(CellHeight, session); text != "" {
builder.add(`grid-template-rows`, text)
}
style.writeViewTransformCSS(builder, session)
if clip := getClipShape(style, Clip, session); clip != nil && clip.valid(session) {
builder.add(`clip-path`, clip.cssStyle(session))
}
if clip := getClipShape(style, ShapeOutside, session); clip != nil && clip.valid(session) {
builder.add(`shape-outside`, clip.cssStyle(session))
}
if value := style.getRaw(Filter); value != nil {
if filter, ok := value.(ViewFilter); ok {
if text := filter.cssStyle(session); text != "" {
builder.add(`filter`, text)
}
}
}
/*
if len(style.transitions) > 0 {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for property, transition := range style.transitions {
if buffer.Len() > 0 {
buffer.WriteString(`, `)
}
buffer.WriteString(property)
transition.cssWrite(buffer, session)
}
if buffer.Len() > 0 {
builder.add(`transition`, buffer.String())
}
}
*/
// TODO text-shadow
}

85
viewStyleGet.go Normal file
View File

@ -0,0 +1,85 @@
package rui
import (
"strings"
)
func getOrientation(style Properties, session Session) (int, bool) {
if value := style.Get(Orientation); value != nil {
switch value := value.(type) {
case int:
return value, true
case string:
text, ok := session.resolveConstants(value)
if !ok {
return 0, false
}
text = strings.ToLower(strings.Trim(text, " \t\n\r"))
switch text {
case "vertical":
return TopDownOrientation, true
case "horizontal":
return StartToEndOrientation, true
}
if result, ok := enumStringToInt(text, enumProperties[Orientation].values, true); ok {
return result, true
}
}
}
return 0, false
}
func (style *viewStyle) Get(tag string) interface{} {
return style.get(strings.ToLower(tag))
}
func (style *viewStyle) get(tag string) interface{} {
switch tag {
case Border, CellBorder:
return getBorder(&style.propertyList, tag)
case BorderLeft, BorderRight, BorderTop, BorderBottom,
BorderStyle, BorderLeftStyle, BorderRightStyle, BorderTopStyle, BorderBottomStyle,
BorderColor, BorderLeftColor, BorderRightColor, BorderTopColor, BorderBottomColor,
BorderWidth, BorderLeftWidth, BorderRightWidth, BorderTopWidth, BorderBottomWidth:
if border := getBorder(style, Border); border != nil {
return border.Get(tag)
}
return nil
case CellBorderLeft, CellBorderRight, CellBorderTop, CellBorderBottom,
CellBorderStyle, CellBorderLeftStyle, CellBorderRightStyle, CellBorderTopStyle, CellBorderBottomStyle,
CellBorderColor, CellBorderLeftColor, CellBorderRightColor, CellBorderTopColor, CellBorderBottomColor,
CellBorderWidth, CellBorderLeftWidth, CellBorderRightWidth, CellBorderTopWidth, CellBorderBottomWidth:
if border := getBorder(style, CellBorder); border != nil {
return border.Get(tag)
}
return nil
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
return getRadiusElement(style, tag)
case ColumnSeparator:
if val, ok := style.properties[ColumnSeparator]; ok {
return val.(ColumnSeparatorProperty)
}
return nil
case ColumnSeparatorStyle, ColumnSeparatorWidth, ColumnSeparatorColor:
if val, ok := style.properties[ColumnSeparator]; ok {
separator := val.(ColumnSeparatorProperty)
return separator.Get(tag)
}
return nil
}
return style.propertyList.getRaw(tag)
}

273
viewStyleSet.go Normal file
View File

@ -0,0 +1,273 @@
package rui
import (
"strings"
)
func (style *viewStyle) setRange(tag string, value interface{}) bool {
switch value := value.(type) {
case string:
if strings.Contains(value, "@") {
style.properties[tag] = value
return true
}
var r Range
if !r.setValue(value) {
invalidPropertyValue(tag, value)
return false
}
style.properties[tag] = r
case int:
style.properties[tag] = Range{First: value, Last: value}
case Range:
style.properties[tag] = value
default:
notCompatibleType(tag, value)
return false
}
return true
}
func (style *viewStyle) setBackground(value interface{}) bool {
switch value := value.(type) {
case BackgroundElement:
style.properties[Background] = []BackgroundElement{value}
return true
case []BackgroundElement:
style.properties[Background] = value
return true
case DataObject:
if element := createBackground(value); element != nil {
style.properties[Background] = []BackgroundElement{element}
return true
}
case []DataObject:
for _, obj := range value {
background := []BackgroundElement{}
if element := createBackground(obj); element != nil {
background = append(background, element)
}
if len(background) > 0 {
style.properties[Background] = background
return true
}
}
case string:
if obj := ParseDataText(value); obj != nil {
if element := createBackground(obj); element != nil {
style.properties[Background] = []BackgroundElement{element}
return true
}
}
}
return false
}
func (style *viewStyle) Remove(tag string) {
style.remove(strings.ToLower(tag))
}
func (style *viewStyle) remove(tag string) {
switch tag {
case BorderStyle, BorderColor, BorderWidth,
BorderLeft, BorderLeftStyle, BorderLeftColor, BorderLeftWidth,
BorderRight, BorderRightStyle, BorderRightColor, BorderRightWidth,
BorderTop, BorderTopStyle, BorderTopColor, BorderTopWidth,
BorderBottom, BorderBottomStyle, BorderBottomColor, BorderBottomWidth:
if border := getBorder(style, Border); border != nil {
border.delete(tag)
}
case CellBorderStyle, CellBorderColor, CellBorderWidth,
CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth,
CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth,
CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth,
CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth:
if border := getBorder(style, CellBorder); border != nil {
border.delete(tag)
}
case MarginTop, MarginRight, MarginBottom, MarginLeft,
"top-margin", "right-margin", "bottom-margin", "left-margin":
style.removeBoundsSide(Margin, tag)
case PaddingTop, PaddingRight, PaddingBottom, PaddingLeft,
"top-padding", "right-padding", "bottom-padding", "left-padding":
style.removeBoundsSide(Padding, tag)
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft:
style.removeBoundsSide(CellPadding, tag)
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
style.removeRadiusElement(tag)
case OutlineStyle, OutlineWidth, OutlineColor:
if outline := getOutline(style); outline != nil {
outline.Remove(tag)
}
default:
style.propertyList.remove(tag)
}
}
func (style *viewStyle) Set(tag string, value interface{}) bool {
return style.set(strings.ToLower(tag), value)
}
func (style *viewStyle) set(tag string, value interface{}) bool {
if value == nil {
style.remove(tag)
return true
}
switch tag {
case Shadow, TextShadow:
return style.setShadow(tag, value)
case Background:
return style.setBackground(value)
case Border, CellBorder:
if border := newBorderProperty(value); border != nil {
style.properties[tag] = border
return true
}
case BorderStyle, BorderColor, BorderWidth,
BorderLeft, BorderLeftStyle, BorderLeftColor, BorderLeftWidth,
BorderRight, BorderRightStyle, BorderRightColor, BorderRightWidth,
BorderTop, BorderTopStyle, BorderTopColor, BorderTopWidth,
BorderBottom, BorderBottomStyle, BorderBottomColor, BorderBottomWidth:
border := getBorder(style, Border)
if border == nil {
border = NewBorder(nil)
if border.Set(tag, value) {
style.properties[Border] = border
return true
}
return false
}
return border.Set(tag, value)
case CellBorderStyle, CellBorderColor, CellBorderWidth,
CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth,
CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth,
CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth,
CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth:
border := getBorder(style, CellBorder)
if border == nil {
border = NewBorder(nil)
if border.Set(tag, value) {
style.properties[CellBorder] = border
return true
}
return false
}
return border.Set(tag, value)
case Radius:
return style.setRadius(value)
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
return style.setRadiusElement(tag, value)
case Margin, Padding, CellPadding:
return style.setBounds(tag, value)
case MarginTop, MarginRight, MarginBottom, MarginLeft,
"top-margin", "right-margin", "bottom-margin", "left-margin":
return style.setBoundsSide(Margin, tag, value)
case PaddingTop, PaddingRight, PaddingBottom, PaddingLeft,
"top-padding", "right-padding", "bottom-padding", "left-padding":
return style.setBoundsSide(Padding, tag, value)
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft:
return style.setBoundsSide(CellPadding, tag, value)
case Outline:
return style.setOutline(value)
case OutlineStyle, OutlineWidth, OutlineColor:
if outline := getOutline(style); outline != nil {
return outline.Set(tag, value)
}
style.properties[Outline] = NewOutlineProperty(Params{tag: value})
return true
case Orientation:
if text, ok := value.(string); ok {
switch strings.ToLower(text) {
case "vertical":
style.properties[Orientation] = TopDownOrientation
return true
case "horizontal":
style.properties[Orientation] = StartToEndOrientation
return true
}
}
case TextWeight:
if n, ok := value.(int); ok && n >= 100 && n%100 == 0 {
n /= 100
if n > 0 && n <= 9 {
style.properties[TextWeight] = StartToEndOrientation
return true
}
}
case Row, Column:
return style.setRange(tag, value)
case CellWidth, CellHeight:
return style.setGridCellSize(tag, value)
case ColumnSeparator:
if separator := newColumnSeparatorProperty(value); separator != nil {
style.properties[ColumnSeparator] = separator
return true
}
return false
case ColumnSeparatorStyle, ColumnSeparatorWidth, ColumnSeparatorColor:
var separator ColumnSeparatorProperty = nil
if val, ok := style.properties[ColumnSeparator]; ok {
separator = val.(ColumnSeparatorProperty)
}
if separator == nil {
separator = newColumnSeparatorProperty(nil)
}
if separator.Set(tag, value) {
style.properties[ColumnSeparator] = separator
return true
}
return false
case Clip, ShapeOutside:
return style.setClipShape(tag, value)
case Filter:
return style.setFilter(value)
}
return style.propertyList.set(tag, value)
}

131
viewStyle_test.go Normal file
View File

@ -0,0 +1,131 @@
package rui
/*
import (
"strings"
"testing"
)
func TestViewStyleCreate(t *testing.T) {
app := new(application)
app.init("")
session := newSession(app, 1, "", false, false)
var style viewStyle
style.init()
data := []struct{ property, value string }{
{Width, "100%"},
{Height, "400px"},
{Margin, "4px"},
{Margin + "-bottom", "auto"},
{Padding, "1em"},
{Font, "Arial"},
{BackgroundColor, "#FF008000"},
{TextColor, "#FF000000"},
{TextSize, "1.25em"},
{TextWeight, "bold"},
{TextAlign, "center"},
{TextTransform, "uppercase"},
{TextIndent, "0.25em"},
{LetterSpacing, "1.5em"},
{WordSpacing, "8px"},
{LineHeight, "2em"},
{Italic, "on"},
{TextDecoration, "strikethrough | overline | underline"},
{SmallCaps, "on"},
}
for _, prop := range data {
style.Set(prop.property, prop.value)
}
style.AddShadow(NewViewShadow(SizeUnit{Auto, 0}, SizeUnit{Auto, 0}, Px(4), Px(6), 0xFF808080))
expected := `width: 100%; height: 400px; font-size: 1.25rem; text-indent: 0.25rem; letter-spacing: 1.5rem; word-spacing: 8px; ` +
`line-height: 2rem; padding: 1rem; margin-left: 4px; margin-top: 4px; margin-right: 4px; box-shadow: 0 0 4px 6px rgb(128,128,128); ` +
`background-color: rgb(0,128,0); color: rgb(0,0,0); font-family: Arial; font-weight: bold; font-style: italic; font-variant: small-caps; ` +
`text-align: center; text-decoration: line-through overline underline; text-transform: uppercase;`
buffer := strings.Builder{}
style.cssViewStyle(&buffer, session)
if text := strings.Trim(buffer.String(), " "); text != expected {
t.Error("\nresult : " + text + "\nexpected: " + expected)
}
w := newCompactDataWriter()
w.StartObject("_")
style.writeStyle(w)
w.FinishObject()
expected2 := `_{width=100%,height=400px,margin="4px,4px,auto,4px",padding=1em,background-color=#FF008000,shadow=_{color=#FF808080,blur=4px,spread-radius=6px},font=Arial,text-color=#FF000000,text-size=1.25em,text-weight=bold,italic=on,small-caps=on,text-decoration=strikethrough|overline|underline,text-align=center,text-indent=0.25em,letter-spacing=1.5em,word-spacing=8px,line-height=2em,text-transform=uppercase}`
if text := w.String(); text != expected2 {
t.Error("\n result: " + text + "\nexpected: " + expected2)
}
var style1 viewStyle
style1.init()
if obj, err := ParseDataText(expected2); err == nil {
style1.parseStyle(obj, new(sessionData))
buffer.Reset()
style.cssStyle(&buffer)
if text := buffer.String(); text != expected {
t.Error("\n result: " + text + "\nexpected: " + expected)
}
} else {
t.Error(err)
}
var style2 viewStyle
style2.init()
style2.textWeight = 4
style2.textAlign = RightAlign
style2.textTransform = LowerCaseTextTransform
style2.textDecoration = NoneDecoration
style2.italic = Off
style2.smallCaps = Off
expected = `font-weight: normal; font-style: normal; font-variant: normal; text-align: right; text-decoration: none; text-transform: lowercase; `
buffer.Reset()
style2.cssStyle(&buffer)
if text := buffer.String(); text != expected {
t.Error("\n result: " + text + "\nexpected: " + expected)
}
w.Reset()
w.StartObject("_")
style2.writeStyle(w)
w.FinishObject()
expected = `_{text-weight=normal,italic=off,small-caps=off,text-decoration=none,text-align=right,text-transform=lowercase}`
if text := w.String(); text != expected {
t.Error("\n result: " + text + "\nexpected: " + expected)
}
style2.textWeight = 5
style2.textAlign = JustifyTextAlign
style2.textTransform = CapitalizeTextTransform
style2.textDecoration = Inherit
style2.italic = Inherit
style2.smallCaps = Inherit
expected = `font-weight: 500; text-align: justify; text-transform: capitalize; `
buffer.Reset()
style2.cssStyle(&buffer)
if text := buffer.String(); text != expected {
t.Error("\n result: " + text + "\nexpected: " + expected)
}
w.Reset()
w.StartObject("_")
style2.writeStyle(w)
w.FinishObject()
expected = `_{text-weight=5,text-align=justify,text-transform=capitalize}`
if text := w.String(); text != expected {
t.Error("\n result: " + text + "\nexpected: " + expected)
}
}
*/

299
viewTransform.go Normal file
View File

@ -0,0 +1,299 @@
package rui
import (
"strconv"
)
const (
// Perspective is the name of the SizeUnit property that determines the distance between the z = 0 plane
// and the user in order to give a 3D-positioned element some perspective. Each 3D element
// with z > 0 becomes larger; each 3D-element with z < 0 becomes smaller.
// The default value is 0 (no 3D effects).
Perspective = "perspective"
// PerspectiveOriginX is the name of the SizeUnit property that determines the x-coordinate of the position
// at which the viewer is looking. It is used as the vanishing point by the Perspective property.
// The default value is 50%.
PerspectiveOriginX = "perspective-origin-x"
// PerspectiveOriginY is the name of the SizeUnit property that determines the y-coordinate of the position
// at which the viewer is looking. It is used as the vanishing point by the Perspective property.
// The default value is 50%.
PerspectiveOriginY = "perspective-origin-y"
// BackfaceVisible is the name of the bool property that sets whether the back face of an element is
// visible when turned towards the user. Values:
// true - the back face is visible when turned towards the user (default value).
// false - the back face is hidden, effectively making the element invisible when turned away from the user.
BackfaceVisible = "backface-visibility"
// OriginX is the name of the SizeUnit property that determines the x-coordinate of the point around which
// a view transformation is applied.
// The default value is 50%.
OriginX = "origin-x"
// OriginY is the name of the SizeUnit property that determines the y-coordinate of the point around which
// a view transformation is applied.
// The default value is 50%.
OriginY = "origin-y"
// OriginZ is the name of the SizeUnit property that determines the z-coordinate of the point around which
// a view transformation is applied.
// The default value is 50%.
OriginZ = "origin-z"
// TranslateX is the name of the SizeUnit property that specify the x-axis translation value
// of a 2D/3D translation
TranslateX = "translate-x"
// TranslateY is the name of the SizeUnit property that specify the y-axis translation value
// of a 2D/3D translation
TranslateY = "translate-y"
// TranslateZ is the name of the SizeUnit property that specify the z-axis translation value
// of a 3D translation
TranslateZ = "translate-z"
// ScaleX is the name of the float property that specify the x-axis scaling value of a 2D/3D scale
// The default value is 1.
ScaleX = "scale-x"
// ScaleY is the name of the float property that specify the y-axis scaling value of a 2D/3D scale
// The default value is 1.
ScaleY = "scale-y"
// ScaleZ is the name of the float property that specify the z-axis scaling value of a 3D scale
// The default value is 1.
ScaleZ = "scale-z"
// Rotate is the name of the AngleUnit property that determines the angle of the view rotation.
// A positive angle denotes a clockwise rotation, a negative angle a counter-clockwise one.
Rotate = "rotate"
// RotateX is the name of the float property that determines the x-coordinate of the vector denoting
// the axis of rotation which could between 0 and 1.
RotateX = "rotate-x"
// RotateY is the name of the float property that determines the y-coordinate of the vector denoting
// the axis of rotation which could between 0 and 1.
RotateY = "rotate-y"
// RotateZ is the name of the float property that determines the z-coordinate of the vector denoting
// the axis of rotation which could between 0 and 1.
RotateZ = "rotate-z"
// SkewX is the name of the AngleUnit property that representing the angle to use to distort
// the element along the abscissa. The default value is 0.
SkewX = "skew-x"
// SkewY is the name of the AngleUnit property that representing the angle to use to distort
// the element along the ordinate. The default value is 0.
SkewY = "skew-y"
)
func getTransform3D(style Properties, session Session) bool {
perspective, ok := sizeProperty(style, Perspective, session)
return ok && perspective.Type != Auto && perspective.Value != 0
}
func getPerspectiveOrigin(style Properties, session Session) (SizeUnit, SizeUnit) {
x, _ := sizeProperty(style, PerspectiveOriginX, session)
y, _ := sizeProperty(style, PerspectiveOriginY, session)
return x, y
}
func getOrigin(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) {
x, _ := sizeProperty(style, OriginX, session)
y, _ := sizeProperty(style, OriginY, session)
z, _ := sizeProperty(style, OriginZ, session)
return x, y, z
}
func getSkew(style Properties, session Session) (AngleUnit, AngleUnit) {
skewX, _ := angleProperty(style, SkewX, session)
skewY, _ := angleProperty(style, SkewY, session)
return skewX, skewY
}
func getTranslate(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) {
x, _ := sizeProperty(style, TranslateX, session)
y, _ := sizeProperty(style, TranslateY, session)
z, _ := sizeProperty(style, TranslateZ, session)
return x, y, z
}
func getScale(style Properties, session Session) (float64, float64, float64) {
scaleX, _ := floatProperty(style, ScaleX, session, 1)
scaleY, _ := floatProperty(style, ScaleY, session, 1)
scaleZ, _ := floatProperty(style, ScaleZ, session, 1)
return scaleX, scaleY, scaleZ
}
func getRotate(style Properties, session Session) (float64, float64, float64, AngleUnit) {
rotateX, _ := floatProperty(style, RotateX, session, 1)
rotateY, _ := floatProperty(style, RotateY, session, 1)
rotateZ, _ := floatProperty(style, RotateZ, session, 1)
angle, _ := angleProperty(style, Rotate, session)
return rotateX, rotateY, rotateZ, angle
}
func (style *viewStyle) transform(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
skewX, skewY := getSkew(style, session)
if skewX.Value != 0 || skewY.Value != 0 {
buffer.WriteString(`skew(`)
buffer.WriteString(skewX.cssString())
buffer.WriteRune(',')
buffer.WriteString(skewY.cssString())
buffer.WriteRune(')')
}
x, y, z := getTranslate(style, session)
scaleX, scaleY, scaleZ := getScale(style, session)
if getTransform3D(style, session) {
if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) || (z.Type != Auto && z.Value != 0) {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(`translate3d(`)
buffer.WriteString(x.cssString("0"))
buffer.WriteRune(',')
buffer.WriteString(y.cssString("0"))
buffer.WriteRune(',')
buffer.WriteString(z.cssString("0"))
buffer.WriteRune(')')
}
if scaleX != 1 || scaleY != 1 || scaleZ != 1 {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(`scale3d(`)
buffer.WriteString(strconv.FormatFloat(scaleX, 'g', -1, 64))
buffer.WriteRune(',')
buffer.WriteString(strconv.FormatFloat(scaleY, 'g', -1, 64))
buffer.WriteRune(',')
buffer.WriteString(strconv.FormatFloat(scaleZ, 'g', -1, 64))
buffer.WriteRune(')')
}
rotateX, rotateY, rotateZ, angle := getRotate(style, session)
if angle.Value != 0 && (rotateX != 0 || rotateY != 0 || rotateZ != 0) {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(`rotate3d(`)
buffer.WriteString(strconv.FormatFloat(rotateX, 'g', -1, 64))
buffer.WriteRune(',')
buffer.WriteString(strconv.FormatFloat(rotateY, 'g', -1, 64))
buffer.WriteRune(',')
buffer.WriteString(strconv.FormatFloat(rotateZ, 'g', -1, 64))
buffer.WriteRune(',')
buffer.WriteString(angle.cssString())
buffer.WriteRune(')')
}
} else {
if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(`translate(`)
buffer.WriteString(x.cssString("0"))
buffer.WriteRune(',')
buffer.WriteString(y.cssString("0"))
buffer.WriteRune(')')
}
if scaleX != 1 || scaleY != 1 {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(`scale(`)
buffer.WriteString(strconv.FormatFloat(scaleX, 'g', -1, 64))
buffer.WriteRune(',')
buffer.WriteString(strconv.FormatFloat(scaleY, 'g', -1, 64))
buffer.WriteRune(')')
}
angle, _ := angleProperty(style, Rotate, session)
if angle.Value != 0 {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(`rotate(`)
buffer.WriteString(angle.cssString())
buffer.WriteRune(')')
}
}
return buffer.String()
}
func (style *viewStyle) writeViewTransformCSS(builder cssBuilder, session Session) {
if getTransform3D(style, session) {
if perspective, ok := sizeProperty(style, Perspective, session); ok && perspective.Type != Auto && perspective.Value != 0 {
builder.add(`perspective`, perspective.cssString("0"))
}
x, y := getPerspectiveOrigin(style, session)
if x.Type != Auto || y.Type != Auto {
builder.addValues(`perspective-origin`, ` `, x.cssString("50%"), y.cssString("50%"))
}
if backfaceVisible, ok := boolProperty(style, BackfaceVisible, session); ok {
if backfaceVisible {
builder.add(`backface-visibility`, `visible`)
} else {
builder.add(`backface-visibility`, `hidden`)
}
}
x, y, z := getOrigin(style, session)
if x.Type != Auto || y.Type != Auto || z.Type != Auto {
builder.addValues(`transform-origin`, ` `, x.cssString("50%"), y.cssString("50%"), z.cssString("0"))
}
} else {
x, y, _ := getOrigin(style, session)
if x.Type != Auto || y.Type != Auto {
builder.addValues(`transform-origin`, ` `, x.cssString("50%"), y.cssString("50%"))
}
}
builder.add(`transform`, style.transform(session))
}
func (view *viewData) updateTransformProperty(tag string) bool {
htmlID := view.htmlID()
session := view.session
switch tag {
case Perspective:
updateCSSStyle(htmlID, session)
case PerspectiveOriginX, PerspectiveOriginY:
if getTransform3D(view, session) {
x, y := GetPerspectiveOrigin(view, "")
value := ""
if x.Type != Auto || y.Type != Auto {
value = x.cssString("50%") + " " + y.cssString("50%")
}
updateCSSProperty(htmlID, "perspective-origin", value, session)
}
case BackfaceVisible:
if getTransform3D(view, session) {
if GetBackfaceVisible(view, "") {
updateCSSProperty(htmlID, BackfaceVisible, "visible", session)
} else {
updateCSSProperty(htmlID, BackfaceVisible, "hidden", session)
}
}
case OriginX, OriginY, OriginZ:
x, y, z := getOrigin(view, session)
value := ""
if getTransform3D(view, session) {
if x.Type != Auto || y.Type != Auto || z.Type != Auto {
value = x.cssString("50%") + " " + y.cssString("50%") + " " + z.cssString("50%")
}
} else {
if x.Type != Auto || y.Type != Auto {
value = x.cssString("50%") + " " + y.cssString("50%")
}
}
updateCSSProperty(htmlID, "transform-origin", value, session)
case SkewX, SkewY, TranslateX, TranslateY, TranslateZ, ScaleX, ScaleY, ScaleZ, Rotate, RotateX, RotateY, RotateZ:
updateCSSProperty(htmlID, "transform", view.transform(session), session)
default:
return false
}
return true
}

Some files were not shown because too many files have changed in this diff Show More