Compare commits

...

19 Commits

Author SHA1 Message Date
anoshenko 918fbf3473 Update httpHandler.go 2024-04-29 12:32:24 +03:00
Alexei Anoshenko cbca1e7c87
Merge pull request #4 from anoshenko/0.14
0.14
2024-04-29 12:28:54 +03:00
Alexei Anoshenko a9f59e3080
Merge branch 'main' into 0.14 2024-04-29 12:28:39 +03:00
anoshenko 345850b552 Added SocketAutoClose property to AppParams 2024-04-27 16:16:30 +03:00
anoshenko dcc69ad777 Added "cell-vertical-self-align", and "cell-horizontal-self-align" properties 2024-04-24 19:26:57 +03:00
anoshenko 8bfa759230 Bug fixing 2024-04-24 13:49:16 +03:00
anoshenko 9ac68ac0c9 Updated docs 2024-04-23 19:34:36 +03:00
anoshenko b1f085b891 Bug fixing 2024-04-23 18:24:51 +03:00
anoshenko 9e4fdccd79 Upgrade go.mod 2024-04-23 12:02:30 +03:00
Alexei Anoshenko 7fd6a7985e Bug fixing 2024-04-22 20:03:40 +03:00
Alexei Anoshenko 50e5b8d44d Bug fixing 2024-04-22 19:17:04 +03:00
Alexei Anoshenko 856b09b04b Fixed "background" property 2024-04-22 19:14:58 +03:00
Alexei Anoshenko 0740a48346 Fixing "hint" property 2024-04-22 16:35:18 +03:00
anoshenko befb2a7484 Added StartTimer and StopTimer to Session 2024-04-22 13:09:35 +03:00
anoshenko 6c49f37f68 Bug fixing 2024-03-13 15:01:02 +03:00
anoshenko ebcba7f9c2 Added NoSocket parameter of the app 2024-03-12 19:32:22 +03:00
Alexei Anoshenko 30c915d73b Bug fixing 2024-02-27 17:08:05 +03:00
Alexei Anoshenko d07b24c953 Added writeMutex to wsBridge 2024-02-10 12:25:01 +03:00
Alexei Anoshenko 5354ec6ea1 Refactoring of js code 2024-01-15 08:13:46 -05:00
45 changed files with 1336 additions and 592 deletions

View File

@ -1,3 +1,14 @@
# v0.14.0
* Added the ability to work without creating a WebSocket. Added NoSocket property to AppParams.
* Added SocketAutoClose property to AppParams.
* Added the ability to run a timer on the client side. Added StartTimer and StopTimer methods to Session interface.
* Added "cell-vertical-self-align", and "cell-horizontal-self-align" properties
* Bug fixing
# v0.13.x
* Added NewHandler function
* Bug fixing
# v0.13.0
* Added SetHotKey function to Session interface

View File

@ -2465,9 +2465,10 @@ The SizeUnit value of type SizeInFraction can be either integer or fractional.
The "grid-row-gap" and "grid-column-gap" SizeUnit properties (GridRowGap and GridColumnGap constants)
allow you to set the distance between the rows and columns of the container, respectively. The default is 0px.
### "cell-vertical-align" property
### "cell-vertical-align" and "cell-vertical-self-align" properties
The "cell-vertical-align" property (constant CellVerticalAlign) of type int sets the vertical alignment of children within the cell they are occupying. Valid values:
The "cell-vertical-align" int property (constant CellVerticalAlign) sets the default vertical alignment of children
within the cell they are occupying. Valid values:
| Value | Constant | Name | Alignment |
|:-----:|--------------|-----------|---------------------|
@ -2478,9 +2479,13 @@ The "cell-vertical-align" property (constant CellVerticalAlign) of type int sets
The default value is StretchAlign (3)
### "cell-horizontal-align" property
The "cell-vertical-self-align" int property (constant CellVerticalAlign) sets the vertical alignment of children
within the cell they are occupying. This property should be set not for the grid, but for the children.
The "cell-horizontal-align" property (constant CellHorizontalAlign) of type int sets the horizontal alignment of children within the occupied cell. Valid values:
### "cell-horizontal-align" and "cell-horizontal-self-align" properties
The "cell-horizontal-align" int property (constant CellHorizontalSelfAlign) sets the horizontal alignment
of children within the occupied cell. Valid values:
| Value | Constant | Name | Alignment |
|:-----:|--------------|-----------|--------------------|
@ -2491,6 +2496,9 @@ The "cell-horizontal-align" property (constant CellHorizontalAlign) of type int
The default value is StretchAlign (3)
The "cell-horizontal-self-align" int property (constant CellVerticalSelfAlign) sets the horizontal alignment of children
within the cell they are occupying. This property should be set not for the grid, but for the children.
## ColumnLayout
ColumnLayout is a container that implements the ViewsContainer interface.

View File

@ -20,6 +20,9 @@ import (
//go:embed app_socket.js
var socketScripts string
//go:embed app_post.js
var httpPostScripts string
func debugLog(text string) {
log.Println("\033[34m" + text)
}
@ -28,11 +31,16 @@ func errorLog(text string) {
log.Println("\033[31m" + text)
}
type sessionInfo struct {
session Session
response chan string
}
type application struct {
server *http.Server
params AppParams
createContentFunc func(Session) SessionContent
sessions map[int]Session
sessions map[int]sessionInfo
}
func (app *application) getStartPage() string {
@ -40,14 +48,26 @@ func (app *application) getStartPage() string {
defer freeStringBuilder(buffer)
buffer.WriteString("<!DOCTYPE html>\n<html>\n")
getStartPage(buffer, app.params, socketScripts)
getStartPage(buffer, app.params)
buffer.WriteString("\n</html>")
return buffer.String()
}
func (app *application) Params() AppParams {
params := app.params
if params.NoSocket {
params.SocketAutoClose = 0
}
return params
}
func (app *application) Finish() {
for _, session := range app.sessions {
session.close()
session.session.close()
if session.response != nil {
close(session.response)
session.response = nil
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@ -69,7 +89,12 @@ func (app *application) nextSessionID() int {
}
func (app *application) removeSession(id int) {
delete(app.sessions, id)
if info, ok := app.sessions[id]; ok {
if info.response != nil {
close(info.response)
}
delete(app.sessions, id)
}
}
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
@ -79,6 +104,11 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
switch req.Method {
case "POST":
if req.URL.Path == "/" {
app.postHandler(w, req)
}
case "GET":
switch req.URL.Path {
case "/":
@ -86,10 +116,20 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, app.getStartPage())
case "/ws":
if bridge := CreateSocketBridge(w, req); bridge != nil {
if bridge := createSocketBridge(w, req); bridge != nil {
go app.socketReader(bridge)
}
case "/script.js":
w.WriteHeader(http.StatusOK)
if app.params.NoSocket {
io.WriteString(w, httpPostScripts)
} else {
io.WriteString(w, socketScripts)
}
io.WriteString(w, "\n")
io.WriteString(w, defaultScripts)
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
@ -104,7 +144,87 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
func (app *application) socketReader(bridge webBridge) {
func setSessionIDCookie(w http.ResponseWriter, sessionID int) {
cookie := http.Cookie{
Name: "session",
Value: strconv.Itoa(sessionID),
HttpOnly: true,
}
http.SetCookie(w, &cookie)
}
func (app *application) postHandler(w http.ResponseWriter, req *http.Request) {
if reqBody, err := io.ReadAll(req.Body); err == nil {
message := string(reqBody)
if ProtocolInDebugLog {
DebugLog(message)
}
if obj := ParseDataText(message); obj != nil {
var session Session = nil
var response chan string = nil
if cookie, err := req.Cookie("session"); err == nil {
sessionID, err := strconv.Atoi(cookie.Value)
if err != nil {
ErrorLog(err.Error())
} else if info, ok := app.sessions[sessionID]; ok && info.response != nil {
response = info.response
session = info.session
}
}
command := obj.Tag()
startSession := false
if session == nil || command == "startSession" {
events := make(chan DataObject, 1024)
bridge := createHttpBridge(req)
response = bridge.response
answer := ""
session, answer = app.startSession(obj, events, bridge, response)
bridge.writeMessage(answer)
session.onStart()
if command == "session-resume" {
session.onResume()
}
bridge.sendResponse()
setSessionIDCookie(w, session.ID())
startSession = true
go sessionEventHandler(session, events, bridge)
}
if !startSession {
switch command {
case "nop":
session.sendResponse()
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
return
default:
if !session.handleAnswer(command, obj) {
session.addToEventsQueue(obj)
}
}
}
io.WriteString(w, <-response)
for len(response) > 0 {
io.WriteString(w, <-response)
}
}
}
}
func (app *application) socketReader(bridge *wsBridge) {
var session Session
events := make(chan DataObject, 1024)
@ -116,7 +236,7 @@ func (app *application) socketReader(bridge webBridge) {
}
if ProtocolInDebugLog {
DebugLog(message)
DebugLog("🖥️ -> " + message)
}
if obj := ParseDataText(message); obj != nil {
@ -124,7 +244,7 @@ func (app *application) socketReader(bridge webBridge) {
switch command {
case "startSession":
answer := ""
if session, answer = app.startSession(obj, events, bridge); session != nil {
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) {
return
}
@ -133,22 +253,18 @@ func (app *application) socketReader(bridge webBridge) {
}
case "reconnect":
session = nil
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
if session = app.sessions[sessionID]; session != nil {
if info, ok := app.sessions[sessionID]; ok {
session = info.session
session.setBridge(events, bridge)
answer := allocStringBuilder()
defer freeStringBuilder(answer)
session.writeInitScript(answer)
if !bridge.writeMessage(answer.String()) {
return
}
session.onReconnect()
go sessionEventHandler(session, events, bridge)
return
session.onReconnect()
} else {
DebugLogF("Session #%d not exists", sessionID)
}
DebugLogF("Session #%d not exists", sessionID)
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
@ -156,25 +272,31 @@ func (app *application) socketReader(bridge webBridge) {
ErrorLog(`"session" key not found`)
}
bridge.writeMessage("restartSession();")
case "answer":
session.handleAnswer(obj)
case "imageLoaded":
session.imageManager().imageLoaded(obj, session)
case "imageError":
session.imageManager().imageLoadError(obj, session)
if session == nil {
/* answer := ""
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, bridge)
bridge.writeMessage("restartSession();")
}
*/
bridge.writeMessage("reloadPage();")
return
}
default:
events <- obj
if !session.handleAnswer(command, obj) {
events <- obj
}
}
}
}
}
func sessionEventHandler(session Session, events chan DataObject, bridge webBridge) {
func sessionEventHandler(session Session, events chan DataObject, bridge bridge) {
for {
data := <-events
@ -194,7 +316,9 @@ func sessionEventHandler(session Session, events chan DataObject, bridge webBrid
}
}
func (app *application) startSession(params DataObject, events chan DataObject, bridge webBridge) (Session, string) {
func (app *application) startSession(params DataObject, events chan DataObject,
bridge bridge, response chan string) (Session, string) {
if app.createContentFunc == nil {
return nil, ""
}
@ -205,7 +329,10 @@ func (app *application) startSession(params DataObject, events chan DataObject,
return nil, ""
}
app.sessions[session.ID()] = session
app.sessions[session.ID()] = sessionInfo{
session: session,
response: response,
}
answer := allocStringBuilder()
defer freeStringBuilder(answer)
@ -229,7 +356,7 @@ var apps = []*application{}
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(application)
app.params = params
app.sessions = map[int]Session{}
app.sessions = map[int]sessionInfo{}
app.createContentFunc = createContentFunc
apps = append(apps, app)

View File

@ -17,7 +17,7 @@ type wasmApp struct {
params AppParams
createContentFunc func(Session) SessionContent
session Session
bridge webBridge
bridge bridge
close chan DataObject
}
@ -25,6 +25,12 @@ func (app *wasmApp) Finish() {
app.session.close()
}
func (app *wasmApp) Params() AppParams {
params := app.params
params.SocketAutoClose = 0
return params
}
func debugLog(text string) {
js.Global().Get("console").Call("log", text)
}
@ -44,17 +50,10 @@ func (app *wasmApp) handleMessage(this js.Value, args []js.Value) any {
case "session-close":
app.close <- obj
case "answer":
app.session.handleAnswer(obj)
case "imageLoaded":
app.session.imageManager().imageLoaded(obj, app.session)
case "imageError":
app.session.imageManager().imageLoadError(obj, app.session)
default:
app.session.handleEvent(command, obj)
if !app.session.handleAnswer(command, obj) {
app.session.handleEvent(command, obj)
}
}
}
}

25
app_post.js Normal file
View File

@ -0,0 +1,25 @@
async function sendMessage(message) {
const response = await fetch('/', {
method : 'POST',
body : message,
"Content-Type" : "text/plain",
});
const text = await response.text();
if (text != "") {
window.eval(text)
}
}
window.onload = function() {
sendMessage( sessionInfo() );
}
window.onfocus = function() {
windowFocus = true
sendMessage( "session-resume{}" );
}
function closeSocket() {
}

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,71 @@
var socket
var socketUrl
let socket
function sendMessage(message) {
if (socket) {
socket.send(message)
if (!socket) {
createSocket(function() {
sendMessage( "reconnect{session=" + sessionID + "}" );
if (!windowFocus) {
windowFocus = true;
sendMessage( "session-resume{session=" + sessionID +"}" );
}
socket.send(message);
});
} else {
socket.send(message);
}
}
window.onload = function() {
socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
function createSocket(onopen) {
let socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
socketUrl += document.location.hostname
var port = document.location.port
const port = document.location.port
if (port) {
socketUrl += ":" + port
}
socketUrl += window.location.pathname + "ws"
socket = new WebSocket(socketUrl);
socket.onopen = socketOpen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onopen = onopen;
socket.onclose = onSocketClose;
socket.onerror = onSocketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
};
}
function socketOpen() {
sendMessage( sessionInfo() );
function closeSocket() {
if (socket) {
socket.close()
}
}
function socketReopen() {
window.onload = createSocket(function() {
sendMessage( sessionInfo() );
});
window.onfocus = function() {
windowFocus = true
if (!socket) {
createSocket(function() {
sendMessage( "reconnect{session=" + sessionID + "}" );
sendMessage( "session-resume{session=" + sessionID +"}" );
});
} else {
sendMessage( "session-resume{session=" + sessionID +"}" );
}
}
function onSocketReopen() {
sendMessage( "reconnect{session=" + sessionID + "}" );
}
function socketReconnect() {
if (!socket) {
socket = new WebSocket(socketUrl);
socket.onopen = socketReopen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
createSocket(onSocketReopen);
}
}
function socketClose(event) {
function onSocketClose(event) {
console.log("socket closed")
socket = null;
if (!event.wasClean && windowFocus) {
@ -53,15 +73,6 @@ function socketClose(event) {
}
}
function socketError(error) {
function onSocketError(error) {
console.log(error);
}
window.onfocus = function(event) {
windowFocus = true
if (!socket) {
socketReconnect()
} else {
sendMessage( "session-resume{session=" + sessionID +"}" );
}
}

View File

@ -57,9 +57,10 @@ button {
textarea {
margin: 2px;
padding: 1px;
padding: 4px;
overflow: auto;
font-size: inherit;
resize: none;
}
ul:focus {

View File

@ -1,5 +1,8 @@
window.onfocus = function(event) {
window.onfocus = function() {
windowFocus = true
sendMessage( "session-resume{session=" + sessionID +"}" );
}
function closeSocket() {
}

View File

@ -17,6 +17,7 @@ var defaultThemeText string
// Application - app interface
type Application interface {
Finish()
Params() AppParams
removeSession(id int)
}
@ -24,23 +25,37 @@ type Application interface {
type AppParams struct {
// Title - title of the app window/tab
Title string
// TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android)
TitleColor Color
// Icon - the icon file name
Icon string
// CertFile - path of a certificate for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
// If the certificate is signed by a certificate authority, the certFile should be the concatenation
// of the server's certificate, any intermediates, and the CA's certificate.
CertFile string
// KeyFile - path of a private key for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
KeyFile string
// Redirect80 - if true then the function of redirect from port 80 to 443 is created
Redirect80 bool
// NoSocket - if true then WebSockets will not be used and information exchange
// between the client and the server will be carried out only via http.
NoSocket bool
// SocketAutoClose - time in seconds after which the socket is automatically closed for an inactive session.
// The countdown begins after the OnPause event arrives.
// If the value of this property is less than or equal to 0 then the socket is not closed.
SocketAutoClose int
}
func getStartPage(buffer *strings.Builder, params AppParams, addScripts string) {
func getStartPage(buffer *strings.Builder, params AppParams) {
buffer.WriteString(`<head>
<meta charset="utf-8">
<title>`)
@ -67,11 +82,7 @@ func getStartPage(buffer *strings.Builder, params AppParams, addScripts string)
buffer.WriteString(appStyles)
buffer.WriteString(`</style>
<style id="ruiAnimations"></style>
<script>
`)
buffer.WriteString(defaultScripts)
buffer.WriteString(addScripts)
buffer.WriteString(`</script>
<script src="/script.js"></script>
</head>
<body id="body" onkeydown="keyDownEvent(this, event)">
<div class="ruiRoot" id="ruiRootView"></div>

View File

@ -1,6 +1,9 @@
package rui
import "strings"
import (
"fmt"
"strings"
)
const (
// NoRepeat is value of the Repeat property of an background image:
@ -61,6 +64,8 @@ const (
// BackgroundElement describes the background element.
type BackgroundElement interface {
Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string
Tag() string
Clone() BackgroundElement
@ -239,3 +244,20 @@ func (image *backgroundImage) cssStyle(session Session) string {
return ""
}
func (image *backgroundImage) writeString(buffer *strings.Builder, indent string) {
image.writeToBuffer(buffer, indent, image.Tag(), []string{
Source,
Width,
Height,
ImageHorizontalAlign,
ImageVerticalAlign,
backgroundFit,
Repeat,
Attachment,
})
}
func (image *backgroundImage) String() string {
return runStringWriter(image)
}

View File

@ -336,3 +336,16 @@ func (gradient *backgroundConicGradient) cssStyle(session Session) string {
return buffer.String()
}
func (gradient *backgroundConicGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
Gradient,
CenterX,
CenterY,
Repeating,
})
}
func (gradient *backgroundConicGradient) String() string {
return runStringWriter(gradient)
}

View File

@ -224,6 +224,33 @@ func (point *BackgroundGradientPoint) color(session Session) (Color, bool) {
return 0, false
}
func (point *BackgroundGradientPoint) String() string {
result := "black"
if point.Color != nil {
switch color := point.Color.(type) {
case string:
result = color
case Color:
result = color.String()
}
}
if point.Pos != nil {
switch value := point.Pos.(type) {
case string:
result += " " + value
case SizeUnit:
if value.Type != Auto {
result += " " + value.String()
}
}
}
return result
}
func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
value, ok := gradient.properties[Gradient]
@ -370,6 +397,18 @@ func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
return buffer.String()
}
func (gradient *backgroundLinearGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
Gradient,
Repeating,
Direction,
})
}
func (gradient *backgroundLinearGradient) String() string {
return runStringWriter(gradient)
}
func (gradient *backgroundRadialGradient) Tag() string {
return "radial-gradient"
}
@ -610,3 +649,17 @@ func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
return buffer.String()
}
func (gradient *backgroundRadialGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
Gradient,
CenterX,
CenterY,
Repeating,
RadialGradientShape,
RadialGradientRadius,
})
}
func (gradient *backgroundRadialGradient) String() string {
return runStringWriter(gradient)
}

View File

@ -34,6 +34,7 @@ func newColorPicker(session Session) View {
func (picker *colorPickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "ColorPicker"
picker.hasHtmlDisabled = true
picker.colorChangedListeners = []func(ColorPicker, Color, Color){}
picker.properties[Padding] = Px(0)
}
@ -153,13 +154,6 @@ func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder
}
}
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":

View File

@ -188,10 +188,6 @@ func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Buil
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)
}

View File

@ -40,6 +40,7 @@ func newDatePicker(session Session) View {
func (picker *datePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "DatePicker"
picker.hasHtmlDisabled = true
picker.dateChangedListeners = []func(DatePicker, time.Time, time.Time){}
}
@ -303,13 +304,6 @@ func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder)
}
}
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":

View File

@ -39,6 +39,7 @@ func newDropDownList(session Session) View {
func (list *dropDownListData) init(session Session) {
list.viewData.init(session)
list.tag = "DropDownList"
list.hasHtmlDisabled = true
list.items = []string{}
list.disabledItems = []any{}
list.dropDownListener = []func(DropDownList, int, int){}
@ -370,13 +371,6 @@ func (list *dropDownListData) htmlProperties(self View, buffer *strings.Builder)
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, old int) {
for _, listener := range list.dropDownListener {
listener(list, number, old)

View File

@ -58,6 +58,7 @@ func newEditView(session Session) View {
func (edit *editViewData) init(session Session) {
edit.viewData.init(session)
edit.hasHtmlDisabled = true
edit.textChangeListeners = []func(EditView, string, string){}
edit.tag = "EditView"
}
@ -466,13 +467,6 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
}
}
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 {
buffer.WriteString(GetText(edit))
@ -517,19 +511,30 @@ func GetHint(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
session := view.Session()
text := ""
if view != nil {
if text, ok := stringProperty(view, Hint, view.Session()); ok {
return text
}
if value := valueFromStyle(view, Hint); value != nil {
if text, ok := value.(string); ok {
if text, ok = view.Session().resolveConstants(text); ok {
return text
var ok bool
text, ok = stringProperty(view, Hint, view.Session())
if !ok {
if value := valueFromStyle(view, Hint); value != nil {
if text, ok = value.(string); ok {
if text, ok = session.resolveConstants(text); !ok {
text = ""
}
} else {
text = ""
}
}
}
}
return ""
if text != "" && !GetNotTranslate(view) {
text, _ = session.GetString(text)
}
return text
}
// GetMaxLength returns a maximal length of EditView. If a maximal length is not limited then 0 is returned

View File

@ -83,6 +83,7 @@ func newFilePicker(session Session) View {
func (picker *filePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "FilePicker"
picker.hasHtmlDisabled = true
picker.files = []FileInfo{}
picker.loader = map[int]func(FileInfo, []byte){}
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
@ -260,13 +261,6 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder)
}
}
func (picker *filePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "fileSelected":

4
go.mod
View File

@ -2,4 +2,6 @@ module github.com/anoshenko/rui
go 1.18
require github.com/gorilla/websocket v1.5.0
require github.com/gorilla/websocket v1.5.1
require golang.org/x/net v0.17.0 // indirect

6
go.sum
View File

@ -1,2 +1,4 @@
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=

View File

@ -5,6 +5,44 @@ import (
"strings"
)
const (
// CellVerticalAlign is the constant for the "cell-vertical-align" property tag.
// The "cell-vertical-align" int property sets the default vertical alignment
// of GridLayout children within the cell they are occupying. Valid values:
// * TopAlign (0) / "top"
// * BottomAlign (1) / "bottom"
// * CenterAlign (2) / "center", and
// * StretchAlign (2) / "stretch"
CellVerticalAlign = "cell-vertical-align"
// CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag.
// The "cell-horizontal-align" int property sets the default horizontal alignment
// of GridLayout children within the occupied cell. Valid values:
// * LeftAlign (0) / "left"
// * RightAlign (1) / "right"
// * CenterAlign (2) / "center"
// * StretchAlign (3) / "stretch"
CellHorizontalAlign = "cell-horizontal-align"
// CellVerticalSelfAlign is the constant for the "cell-vertical-self-align" property tag.
// The "cell-vertical-align" int property sets the vertical alignment of GridLayout children
// within the cell they are occupying. The property is set for the child view of GridLayout. Valid values:
// * TopAlign (0) / "top"
// * BottomAlign (1) / "bottom"
// * CenterAlign (2) / "center", and
// * StretchAlign (2) / "stretch"
CellVerticalSelfAlign = "cell-vertical-self-align"
// CellHorizontalSelfAlign is the constant for the "cell-horizontal-self-align" property tag.
// The "cell-horizontal-self align" int property sets the horizontal alignment of GridLayout children
// within the occupied cell. The property is set for the child view of GridLayout. Valid values:
// * LeftAlign (0) / "left"
// * RightAlign (1) / "right"
// * CenterAlign (2) / "center"
// * StretchAlign (3) / "stretch"
CellHorizontalSelfAlign = "cell-horizontal-self-align"
)
// GridLayout - grid-container of View
type GridLayout interface {
ViewsContainer

View File

@ -42,7 +42,7 @@ Example for echo:
func NewHandler(urlPrefix string, createContentFunc func(Session) SessionContent, params AppParams) *httpHandler {
app := new(application)
app.params = params
app.sessions = map[int]Session{}
app.sessions = map[int]sessionInfo{}
app.createContentFunc = createContentFunc
apps = append(apps, app)

View File

@ -80,10 +80,11 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
manager.images[url] = image
session.callFunc("loadImage", url)
session.sendResponse()
return image
}
func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
func (manager *imageManager) imageLoaded(obj DataObject) {
if manager.images == nil {
manager.images = make(map[string]*imageData)
return
@ -109,7 +110,7 @@ func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
}
}
func (manager *imageManager) imageLoadError(obj DataObject, session Session) {
func (manager *imageManager) imageLoadError(obj DataObject) {
if manager.images == nil {
manager.images = make(map[string]*imageData)
return

View File

@ -2,8 +2,13 @@ package rui
// ListAdapter - the list data source
type ListAdapter interface {
// ListSize returns the number of elements in the list
ListSize() int
// ListItem creates a View of a list item at the given index
ListItem(index int, session Session) View
// IsListItemEnabled returns the status (enabled/disabled) of a list item at the given index
IsListItemEnabled(index int) bool
}

View File

@ -7,16 +7,22 @@ import (
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
// ListWrapOff - subviews are scrolled and "true" if a new row/column starts
ListWrapOff = 0
// ListWrapOn - the new row/column starts at bottom/right
ListWrapOn = 1
// ListWrapReverse - the new row/column starts at top/left
ListWrapReverse = 2
)

View File

@ -11,20 +11,25 @@ const (
// The "list-item-clicked" event occurs when the user clicks on an item in the list.
// The main listener format: func(ListView, int), where the second argument is the item index.
ListItemClickedEvent = "list-item-clicked"
// ListItemSelectedEvent is the constant for "list-item-selected" property tag.
// The "list-item-selected" event occurs when a list item becomes selected.
// The main listener format: func(ListView, int), where the second argument is the item index.
ListItemSelectedEvent = "list-item-selected"
// ListItemCheckedEvent is the constant for "list-item-checked" property tag.
// The "list-item-checked" event occurs when a list item checkbox becomes checked/unchecked.
// The main listener format: func(ListView, []int), where the second argument is the array of checked item indexes.
ListItemCheckedEvent = "list-item-checked"
// ListItemStyle is the constant for "list-item-style" property tag.
// The "list-item-style" string property defines the style of an unselected item
ListItemStyle = "list-item-style"
// CurrentStyle is the constant for "current-style" property tag.
// The "current-style" string property defines the style of the selected item when the ListView is focused.
CurrentStyle = "current-style"
// CurrentInactiveStyle is the constant for "current-inactive-style" property tag.
// The "current-inactive-style" string property defines the style of the selected item when the ListView is unfocused.
CurrentInactiveStyle = "current-inactive-style"
@ -589,7 +594,7 @@ func (listView *listViewData) getItemFrames() []Frame {
return listView.itemFrame
}
func (listView *listViewData) itemAlign(self View, buffer *strings.Builder) {
func (listView *listViewData) itemAlign(buffer *strings.Builder) {
values := enumProperties[ItemHorizontalAlign].cssValues
if hAlign := GetListItemHorizontalAlign(listView); hAlign >= 0 && hAlign < len(values) {
buffer.WriteString(" justify-items: ")
@ -605,7 +610,7 @@ func (listView *listViewData) itemAlign(self View, buffer *strings.Builder) {
}
}
func (listView *listViewData) itemSize(self View, buffer *strings.Builder) {
func (listView *listViewData) itemSize(buffer *strings.Builder) {
if itemWidth := GetListItemWidth(listView); itemWidth.Type != Auto {
buffer.WriteString(` min-width: `)
buffer.WriteString(itemWidth.cssString("", listView.Session()))
@ -619,14 +624,14 @@ func (listView *listViewData) itemSize(self View, buffer *strings.Builder) {
}
}
func (listView *listViewData) getDivs(self View, checkbox, hCheckboxAlign, vCheckboxAlign int) (string, string, string) {
func (listView *listViewData) getDivs(checkbox, hCheckboxAlign, vCheckboxAlign int) (string, string, string) {
session := listView.Session()
contentBuilder := allocStringBuilder()
defer freeStringBuilder(contentBuilder)
contentBuilder.WriteString(`<div style="display: grid;`)
listView.itemAlign(self, contentBuilder)
listView.itemAlign(contentBuilder)
onDivBuilder := allocStringBuilder()
defer freeStringBuilder(onDivBuilder)
@ -681,7 +686,7 @@ func (listView *listViewData) getDivs(self View, checkbox, hCheckboxAlign, vChec
return onDivBuilder.String(), offDivBuilder.String(), contentBuilder.String()
}
func (listView *listViewData) checkboxItemDiv(self View, checkbox, hCheckboxAlign, vCheckboxAlign int) string {
func (listView *listViewData) checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign int) string {
itemStyleBuilder := allocStringBuilder()
defer freeStringBuilder(itemStyleBuilder)
@ -760,15 +765,15 @@ func (listView *listViewData) currentInactiveStyle() string {
return listView.itemStyle(CurrentInactiveStyle, "ruiListItemSelected")
}
func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builder, checkbox int) {
func (listView *listViewData) checkboxSubviews(buffer *strings.Builder, checkbox int) {
count := listView.adapter.ListSize()
listViewID := listView.htmlID()
hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView)
vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView)
itemDiv := listView.checkboxItemDiv(self, checkbox, hCheckboxAlign, vCheckboxAlign)
onDiv, offDiv, contentDiv := listView.getDivs(self, checkbox, hCheckboxAlign, vCheckboxAlign)
itemDiv := listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign)
onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign)
current := GetCurrent(listView)
checkedItems := GetListViewCheckedItems(listView)
@ -784,7 +789,7 @@ func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builde
buffer.WriteString(listView.currentInactiveStyle())
}
buffer.WriteString(`" onclick="listItemClickEvent(this, event)" data-left="0" data-top="0" data-width="0" data-height="0" style="display: grid; justify-items: stretch; align-items: stretch;`)
listView.itemSize(self, buffer)
listView.itemSize(buffer)
if !listView.adapter.IsListItemEnabled(i) {
buffer.WriteString(`" data-disabled="1`)
}
@ -815,7 +820,7 @@ func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builde
}
}
func (listView *listViewData) noneCheckboxSubviews(self View, buffer *strings.Builder) {
func (listView *listViewData) noneCheckboxSubviews(buffer *strings.Builder) {
count := listView.adapter.ListSize()
listViewID := listView.htmlID()
@ -824,8 +829,8 @@ func (listView *listViewData) noneCheckboxSubviews(self View, buffer *strings.Bu
itemStyleBuilder.WriteString(`data-left="0" data-top="0" data-width="0" data-height="0" style="max-width: 100%; max-height: 100%; display: grid;`)
listView.itemAlign(self, itemStyleBuilder)
listView.itemSize(self, itemStyleBuilder)
listView.itemAlign(itemStyleBuilder)
listView.itemSize(itemStyleBuilder)
itemStyleBuilder.WriteString(`" onclick="listItemClickEvent(this, event)"`)
itemStyle := itemStyleBuilder.String()
@ -865,12 +870,12 @@ func (listView *listViewData) updateCheckboxItem(index int, checked bool) {
checkbox := GetListViewCheckbox(listView)
hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView)
vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView)
onDiv, offDiv, contentDiv := listView.getDivs(listView, checkbox, hCheckboxAlign, vCheckboxAlign)
onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign)
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(listView.checkboxItemDiv(listView, checkbox, hCheckboxAlign, vCheckboxAlign))
buffer.WriteString(listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign))
if checked {
buffer.WriteString(onDiv)
} else {
@ -1061,9 +1066,9 @@ func (listView *listViewData) htmlSubviews(self View, buffer *strings.Builder) {
checkbox := GetListViewCheckbox(listView)
if checkbox == NoneCheckbox {
listView.noneCheckboxSubviews(self, buffer)
listView.noneCheckboxSubviews(buffer)
} else {
listView.checkboxSubviews(self, buffer, checkbox)
listView.checkboxSubviews(buffer, checkbox)
}
buffer.WriteString(`</div>`)

View File

@ -13,15 +13,18 @@ const (
// to control audio/video playback, including volume, seeking, and pause/resume playback.
// Its default value is false.
Controls = "controls"
// Loop is the constant for the "loop" property tag.
// If the "loop" bool property is "true", the audio/video player will automatically seek back
// to the start upon reaching the end of the audio/video.
// Its default value is false.
Loop = "loop"
// Muted is the constant for the "muted" property tag.
// The "muted" bool property indicates whether the audio/video will be initially silenced.
// Its default value is false.
Muted = "muted"
// Preload is the constant for the "preload" property tag.
// The "preload" int property is intended to provide a hint to the browser about what
// the author thinks will lead to the best user experience. It may have one of the following values:
@ -32,72 +35,94 @@ const (
// AbortEvent is the constant for the "abort-event" property tag.
// The "abort-event" event fired when the resource was not fully loaded, but not as the result of an error.
AbortEvent = "abort-event"
// CanPlayEvent is the constant for the "can-play-event" property tag.
// The "can-play-event" event occurs when the browser can play the media, but estimates that not enough data has been
// loaded to play the media up to its end without having to stop for further buffering of content.
CanPlayEvent = "can-play-event"
// CanPlayThroughEvent is the constant for the "can-play-through-event" property tag.
// The "can-play-through-event" event occurs when the browser estimates it can play the media up
// to its end without stopping for content buffering.
CanPlayThroughEvent = "can-play-through-event"
// CompleteEvent is the constant for the "complete-event" property tag.
// The "complete-event" event occurs when the rendering of an OfflineAudioContext is terminated.
CompleteEvent = "complete-event"
// DurationChangedEvent is the constant for the "duration-changed-event" property tag.
// The "duration-changed-event" event occurs when the duration attribute has been updated.
DurationChangedEvent = "duration-changed-event"
// EmptiedEvent is the constant for the "emptied-event" property tag.
// The "emptied-event" event occurs when the media has become empty; for example, this event is sent if the media has already been loaded
// (or partially loaded), and the HTMLMediaElement.load method is called to reload it.
EmptiedEvent = "emptied-event"
// EndedEvent is the constant for the "ended-event" property tag.
// The "ended-event" event occurs when the playback has stopped because the end of the media was reached.
EndedEvent = "ended-event"
// LoadedDataEvent is the constant for the "loaded-data-event" property tag.
// The "loaded-data-event" event occurs when the first frame of the media has finished loading.
LoadedDataEvent = "loaded-data-event"
// LoadedMetadataEvent is the constant for the "loaded-metadata-event" property tag.
// The "loaded-metadata-event" event occurs when the metadata has been loaded.
LoadedMetadataEvent = "loaded-metadata-event"
// LoadStartEvent is the constant for the "load-start-event" property tag.
// The "load-start-event" event is fired when the browser has started to load a resource.
LoadStartEvent = "load-start-event"
// PauseEvent is the constant for the "pause-event" property tag.
// The "pause-event" event occurs when the playback has been paused.
PauseEvent = "pause-event"
// PlayEvent is the constant for the "play-event" property tag.
// The "play-event" event occurs when the playback has begun.
PlayEvent = "play-event"
// PlayingEvent is the constant for the "playing-event" property tag.
// The "playing-event" event occurs when the playback is ready to start after having been paused or delayed due to lack of data.
PlayingEvent = "playing-event"
// ProgressEvent is the constant for the "progress-event" property tag.
// The "progress-event" event is fired periodically as the browser loads a resource.
ProgressEvent = "progress-event"
// RateChangeEvent is the constant for the "rate-change-event" property tag.
// The "rate-change-event" event occurs when the playback rate has changed.
RateChangedEvent = "rate-changed-event"
// SeekedEvent is the constant for the "seeked-event" property tag.
// The "seeked-event" event occurs when a seek operation completed.
SeekedEvent = "seeked-event"
// SeekingEvent is the constant for the "seeking-event" property tag.
// The "seeking-event" event occurs when a seek operation began.
SeekingEvent = "seeking-event"
// StalledEvent is the constant for the "stalled-event" property tag.
// The "stalled-event" event occurs when the user agent is trying to fetch media data, but data is unexpectedly not forthcoming.
StalledEvent = "stalled-event"
// SuspendEvent is the constant for the "suspend-event" property tag.
// The "suspend-event" event occurs when the media data loading has been suspended.
SuspendEvent = "suspend-event"
// TimeUpdateEvent is the constant for the "time-update-event" property tag.
// The "time-update-event" event occurs when the time indicated by the currentTime attribute has been updated.
TimeUpdateEvent = "time-update-event"
// VolumeChangedEvent is the constant for the "volume-change-event" property tag.
// The "volume-change-event" event occurs when the volume has changed.
VolumeChangedEvent = "volume-changed-event"
// WaitingEvent is the constant for the "waiting-event" property tag.
// The "waiting-event" event occurs when the playback has stopped because of a temporary lack of data
WaitingEvent = "waiting-event"
// PlayerErrorEvent is the constant for the "player-error-event" property tag.
// The "player-error-event" event is fired when the resource could not be loaded due to an error
// (for example, a network connectivity problem).
@ -105,51 +130,68 @@ const (
// PreloadNone - value of the view "preload" property: indicates that the audio/video should not be preloaded.
PreloadNone = 0
// PreloadMetadata - value of the view "preload" property: indicates that only audio/video metadata (e.g. length) is fetched.
PreloadMetadata = 1
// PreloadAuto - value of the view "preload" property: indicates that the whole audio file can be downloaded,
// even if the user is not expected to use it.
PreloadAuto = 2
// PlayerErrorUnknown - MediaPlayer error code: An unknown error.
PlayerErrorUnknown = 0
// PlayerErrorAborted - MediaPlayer error code: The fetching of the associated resource was aborted by the user's request.
PlayerErrorAborted = 1
// PlayerErrorNetwork - MediaPlayer error code: Some kind of network error occurred which prevented the media
// from being successfully fetched, despite having previously been available.
PlayerErrorNetwork = 2
// PlayerErrorDecode - MediaPlayer error code: Despite having previously been determined to be usable,
// an error occurred while trying to decode the media resource, resulting in an error.
PlayerErrorDecode = 3
// PlayerErrorSourceNotSupported - MediaPlayer error code: The associated resource or media provider object has been found to be unsuitable.
PlayerErrorSourceNotSupported = 4
)
type MediaPlayer interface {
View
// Play attempts to begin playback of the media.
Play()
// Pause will pause playback of the media, if the media is already in a paused state this method will have no effect.
Pause()
// SetCurrentTime sets the current playback time in seconds.
SetCurrentTime(seconds float64)
// CurrentTime returns the current playback time in seconds.
CurrentTime() float64
// Duration returns the value indicating the total duration of the media in seconds.
// If no media data is available, the returned value is NaN.
Duration() float64
// SetPlaybackRate sets the rate at which the media is being played back. This is used to implement user controls
// for fast forward, slow motion, and so forth. The normal playback rate is multiplied by this value to obtain
// the current rate, so a value of 1.0 indicates normal speed.
SetPlaybackRate(rate float64)
// PlaybackRate returns the rate at which the media is being played back.
PlaybackRate() float64
// SetVolume sets the audio volume, from 0.0 (silent) to 1.0 (loudest).
SetVolume(volume float64)
// Volume returns the audio volume, from 0.0 (silent) to 1.0 (loudest).
Volume() float64
// IsEnded function tells whether the media element is ended.
IsEnded() bool
// IsPaused function tells whether the media element is paused.
IsPaused() bool
}

View File

@ -82,24 +82,32 @@ const (
// 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
)

View File

@ -7,17 +7,37 @@ import (
)
const (
// NumberChangedEvent is the constant for the "" property tag.
// The "number-changed" property sets listener(s) that track the change in the entered value.
NumberChangedEvent = "number-changed"
NumberPickerType = "number-picker-type"
NumberPickerMin = "number-picker-min"
NumberPickerMax = "number-picker-max"
NumberPickerStep = "number-picker-step"
NumberPickerValue = "number-picker-value"
// NumberPickerType is the constant for the "number-picker-type" property tag.
// The "number-picker-type" int property sets the mode of NumberPicker. It can take the following values:
// * NumberEditor (0) - NumberPicker is presented by editor. Default value;
// * NumberSlider (1) - NumberPicker is presented by slider. |
NumberPickerType = "number-picker-type"
// NumberPickerMin is the constant for the "number-picker-min" property tag.
// The "number-picker-min" int property sets the minimum value of NumberPicker. The default value is 0.
NumberPickerMin = "number-picker-min"
// NumberPickerMax is the constant for the "number-picker-max" property tag.
// The "number-picker-max" int property sets the maximum value of NumberPicker. The default value is 1.
NumberPickerMax = "number-picker-max"
// NumberPickerStep is the constant for the "number-picker-step" property tag.
// The "number-picker-step" int property sets the value change step of NumberPicker
NumberPickerStep = "number-picker-step"
// NumberPickerValue is the constant for the "number-picker-value" property tag.
// The "number-picker-value" int property sets the current value of NumberPicker. The default value is 0.
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
)
@ -47,6 +67,7 @@ func newNumberPicker(session Session) View {
func (picker *numberPickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "NumberPicker"
picker.hasHtmlDisabled = true
picker.numberChangedListeners = []func(NumberPicker, float64, float64){}
}
@ -232,13 +253,6 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde
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":

View File

@ -11,15 +11,19 @@ type Properties interface {
// The type of return value depends on the property. If the property is not set then nil is returned.
Get(tag string) any
getRaw(tag string) any
// 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 any) bool
setRaw(tag string, value any)
// 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
}
@ -68,6 +72,28 @@ func (properties *propertyList) AllTags() []string {
return tags
}
func (properties *propertyList) writeToBuffer(buffer *strings.Builder,
indent string, objectTag string, tags []string) {
buffer.WriteString(objectTag)
buffer.WriteString(" {\n")
indent2 := indent + "\t"
for _, tag := range tags {
if value, ok := properties.properties[tag]; ok {
buffer.WriteString(indent2)
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent2)
buffer.WriteString(",\n")
}
}
buffer.WriteString(indent)
buffer.WriteString("}")
}
func parseProperties(properties Properties, object DataObject) {
count := object.PropertyCount()
for i := 0; i < count; i++ {

View File

@ -666,7 +666,8 @@ const (
// Resize is the constant for the "resize" property tag.
// The "resize" int property sets whether an element is resizable, and if so, in which directions.
// Valid values are "none" (0), "both" (1), horizontal (2), and "vertical" (3)
// Valid values are "none" / NoneResize (0), "both" / BothResize (1),
// "horizontal" / HorizontalResize (2), and "vertical" / VerticalResize (3)
Resize = "resize"
// UserSelect is the constant for the "user-select" property tag.

View File

@ -331,6 +331,16 @@ var enumProperties = map[string]struct {
"justify-items",
[]string{"start", "end", "center", "stretch"},
},
CellVerticalSelfAlign: {
[]string{"top", "bottom", "center", "stretch"},
"align-self",
[]string{"start", "end", "center", "stretch"},
},
CellHorizontalSelfAlign: {
[]string{"left", "right", "center", "stretch"},
"justify-self",
[]string{"start", "end", "center", "stretch"},
},
GridAutoFlow: {
[]string{"row", "column", "row-dense", "column-dense"},
GridAutoFlow,

View File

@ -11,22 +11,23 @@ const (
// 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
)

View File

@ -7,7 +7,7 @@ import (
"strings"
)
type webBridge interface {
type bridge interface {
startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string)
callFunc(funcName string, args ...any) bool
@ -16,10 +16,9 @@ type webBridge interface {
updateCSSProperty(htmlID, property, value string)
updateProperty(htmlID, property string, value any)
removeProperty(htmlID, property string)
readMessage() (string, bool)
writeMessage(text string) bool
addAnimationCSS(css string)
clearAnimation()
sendResponse()
setAnimationCSS(css string)
appendAnimationCSS(css string)
canvasStart(htmlID string)
callCanvasFunc(funcName string, args ...any)
callCanvasVarFunc(v any, funcName string, args ...any)
@ -111,6 +110,14 @@ type Session interface {
// Invoke SetHotKey(..., ..., nil) for remove hotkey function.
SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session))
// StartTimer starts a timer on the client side.
// The first argument specifies the timer period in milliseconds.
// The second argument specifies a function that will be called on each timer event.
// The result is the id of the timer, which is used to stop the timer
StartTimer(ms int, timerFunc func(Session)) int
// StopTimer the timer with the given id
StopTimer(timerID int)
getCurrentTheme() Theme
registerAnimation(props []AnimatedProperty) string
@ -124,7 +131,7 @@ type Session interface {
nextViewID() string
styleProperty(styleTag, property string) any
setBridge(events chan DataObject, bridge webBridge)
setBridge(events chan DataObject, bridge bridge)
writeInitScript(writer *strings.Builder)
callFunc(funcName string, args ...any)
updateInnerHTML(htmlID, html string)
@ -134,6 +141,7 @@ type Session interface {
removeProperty(htmlID, property string)
startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string)
sendResponse()
addAnimationCSS(css string)
clearAnimation()
canvasStart(htmlID string)
@ -145,7 +153,8 @@ type Session interface {
canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string
handleAnswer(data DataObject)
addToEventsQueue(data DataObject)
handleAnswer(command string, data DataObject) bool
handleRootSize(data DataObject)
handleResize(data DataObject)
handleEvent(command string, data DataObject)
@ -189,13 +198,16 @@ type sessionData struct {
ignoreUpdates bool
popups *popupManager
images *imageManager
bridge webBridge
bridge bridge
events chan DataObject
animationCounter int
animationCSS string
updateScripts map[string]*strings.Builder
clientStorage map[string]string
hotkeys map[string]func(Session)
timers map[int]func(Session)
nextTimerID int
pauseTime int64
}
func newSession(app Application, id int, customTheme string, params DataObject) Session {
@ -214,6 +226,8 @@ func newSession(app Application, id int, customTheme string, params DataObject)
session.updateScripts = map[string]*strings.Builder{}
session.clientStorage = map[string]string{}
session.hotkeys = map[string]func(Session){}
session.timers = map[int]func(Session){}
session.nextTimerID = 1
if customTheme != "" {
if theme, ok := CreateThemeFromText(customTheme); ok {
@ -237,7 +251,7 @@ func (session *sessionData) ID() int {
return session.sessionID
}
func (session *sessionData) setBridge(events chan DataObject, bridge webBridge) {
func (session *sessionData) setBridge(events chan DataObject, bridge bridge) {
session.events = events
session.bridge = bridge
}
@ -330,23 +344,19 @@ func (session *sessionData) updateTooltipConstants() {
}
func (session *sessionData) reload() {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
css = strings.ReplaceAll(css, "\n", `\n`)
css = strings.ReplaceAll(css, "\t", `\t`)
buffer.WriteString(`document.querySelector('style').textContent = "`)
buffer.WriteString(css)
buffer.WriteString("\";\n")
session.bridge.callFunc("setStyles", css)
if session.rootView != nil {
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer)
buffer.WriteString("';\nscanElementsSize();")
session.bridge.updateInnerHTML("ruiRootView", buffer.String())
session.bridge.callFunc("scanElementsSize")
}
session.bridge.writeMessage(buffer.String())
session.updateTooltipConstants()
}
@ -447,15 +457,21 @@ func (session *sessionData) finishUpdateScript(htmlID string) {
}
}
func (session *sessionData) sendResponse() {
if session.bridge != nil {
session.bridge.sendResponse()
}
}
func (session *sessionData) addAnimationCSS(css string) {
if session.bridge != nil {
session.bridge.addAnimationCSS(css)
session.bridge.appendAnimationCSS(css)
}
}
func (session *sessionData) clearAnimation() {
if session.bridge != nil {
session.bridge.clearAnimation()
session.bridge.setAnimationCSS("")
}
}
@ -520,8 +536,27 @@ func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
return ""
}
func (session *sessionData) handleAnswer(data DataObject) {
session.bridge.answerReceived(data)
func (session *sessionData) handleAnswer(command string, data DataObject) bool {
switch command {
case "answer":
if session.bridge != nil {
session.bridge.answerReceived(data)
}
case "imageLoaded":
session.imageManager().imageLoaded(data)
case "imageError":
session.imageManager().imageLoadError(data)
default:
return false
}
if session.bridge != nil {
session.bridge.sendResponse()
}
return true
}
func (session *sessionData) handleRootSize(data DataObject) {
@ -642,6 +677,22 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
case "session-resume":
session.onResume()
case "timer":
if text, ok := data.PropertyValue("timerID"); ok {
timerID, err := strconv.Atoi(text)
if err == nil {
if fn, ok := session.timers[timerID]; ok {
fn(session)
} else {
ErrorLog(`Timer (id = ` + text + `) not exists`)
}
} else {
ErrorLog(err.Error())
}
} else {
ErrorLog(`"timerID" property not found`)
}
case "root-size":
session.handleRootSize(data)
@ -672,6 +723,8 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
ErrorLog(`"id" property not found. Event: ` + command)
}
}
session.bridge.sendResponse()
}
func (session *sessionData) hotKey(event KeyEvent) {
@ -769,3 +822,25 @@ func (session *sessionData) RemoveAllClientItems() {
session.clientStorage = map[string]string{}
session.bridge.callFunc("localStorageClear")
}
func (session *sessionData) addToEventsQueue(data DataObject) {
session.events <- data
}
func (session *sessionData) StartTimer(ms int, timerFunc func(Session)) int {
timerID := 0
if session.bridge != nil {
timerID = session.nextTimerID
session.nextTimerID++
session.timers[timerID] = timerFunc
session.bridge.callFunc("startTimer", ms, timerID)
}
return timerID
}
func (session *sessionData) StopTimer(timerID int) {
if session.bridge != nil {
session.bridge.callFunc("stopTimer", timerID)
delete(session.timers, timerID)
}
}

View File

@ -1,5 +1,7 @@
package rui
import "time"
// SessionStartListener is the listener interface of a session start event
type SessionStartListener interface {
OnStart(session Session)
@ -50,13 +52,25 @@ func (session *sessionData) onFinish() {
func (session *sessionData) onPause() {
if session.content != nil {
session.pauseTime = time.Now().Unix()
if listener, ok := session.content.(SessionPauseListener); ok {
listener.OnPause(session)
}
if timeout := session.app.Params().SocketAutoClose; timeout > 0 {
go session.autoClose(session.pauseTime, timeout)
}
}
}
func (session *sessionData) autoClose(start int64, timeout int) {
time.Sleep(time.Second * time.Duration(timeout))
if session.pauseTime == start {
session.bridge.callFunc("closeSocket")
}
}
func (session *sessionData) onResume() {
session.pauseTime = 0
if session.content != nil {
if listener, ok := session.content.(SessionResumeListener); ok {
listener.OnResume(session)

View File

@ -22,10 +22,33 @@ const (
// StackLayout - list-container of View
type StackLayout interface {
ViewsContainer
// Peek returns the current (visible) View. If StackLayout is empty then it returns nil.
Peek() View
// RemovePeek removes the current View and returns it. If StackLayout is empty then it doesn't do anything and returns nil.
RemovePeek() View
// MoveToFront makes the given View current. Returns true if successful, false otherwise.
MoveToFront(view View) bool
// MoveToFrontByID makes the View current by viewID. Returns true if successful, false otherwise.
MoveToFrontByID(viewID string) bool
// Push adds a new View to the container and makes it current.
// It is similar to Append, but the addition is done using an animation effect.
// The animation type is specified by the second argument and can take the following values:
// * DefaultAnimation (0) - Default animation. For the Push function it is EndToStartAnimation, for Pop - StartToEndAnimation;
// * StartToEndAnimation (1) - Animation from beginning to end. The beginning and the end are determined by the direction of the text output;
// * EndToStartAnimation (2) - End-to-Beginning animation;
// * TopDownAnimation (3) - Top-down animation;
// * BottomUpAnimation (4) - Bottom up animation.
// The third argument `onPushFinished` is the function to be called when the animation ends. It may be nil.
Push(view View, animation int, onPushFinished func())
// Pop removes the current View from the container using animation.
// The second argument `onPopFinished`` is the function to be called when the animation ends. It may be nil.
// The function will return false if the StackLayout is empty and true if the current item has been removed.
Pop(animation int, onPopFinished func(View)) bool
}
@ -277,6 +300,10 @@ func (layout *stackLayoutData) RemoveView(index int) View {
return layout.viewsContainerData.RemoveView(index)
}
func (layout *stackLayoutData) RemovePeek() View {
return layout.RemoveView(len(layout.views) - 1)
}
func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) {
if view == nil {
ErrorLog("StackLayout.Push(nil, ....) is forbidden")

View File

@ -40,6 +40,7 @@ func newTimePicker(session Session) View {
func (picker *timePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "TimePicker"
picker.hasHtmlDisabled = true
picker.timeChangedListeners = []func(TimePicker, time.Time, time.Time){}
}
@ -291,13 +292,6 @@ func (picker *timePickerData) htmlProperties(self View, buffer *strings.Builder)
}
}
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":

View File

@ -8,9 +8,11 @@ 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,

87
view.go
View File

@ -35,24 +35,33 @@ type View interface {
// Session returns the current Session interface
Session() Session
// Parent returns the parent view
Parent() View
// 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 scrollable 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 any, animation Animation) bool
// SetChangeListener set the function to track the change of the View property
SetChangeListener(tag string, listener func(View, string))
// HasFocus returns 'true' if the view has focus
HasFocus() bool
@ -65,7 +74,6 @@ type View interface {
setParentID(parentID 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)
@ -93,6 +101,7 @@ type viewData struct {
noResizeEvent bool
created bool
hasFocus bool
hasHtmlDisabled bool
//animation map[string]AnimationEndListener
}
@ -135,6 +144,7 @@ func (view *viewData) init(session Session) {
view.singleTransition = map[string]Animation{}
view.noResizeEvent = false
view.created = false
view.hasHtmlDisabled = false
}
func (view *viewData) Session() Session {
@ -302,7 +312,6 @@ func (view *viewData) propertyChangedEvent(tag string) {
if listener, ok := view.changeListener[tag]; ok {
listener(view, tag)
}
}
func (view *viewData) Set(tag string, value any) bool {
@ -404,7 +413,35 @@ func viewPropertyChanged(view *viewData, tag string) {
switch tag {
case Disabled:
updateInnerHTML(view.parentHTMLID(), session)
tabIndex := GetTabIndex(view, htmlID)
enabledClass := view.htmlClass(false)
disabledClass := view.htmlClass(true)
session.startUpdateScript(htmlID)
if IsDisabled(view) {
session.updateProperty(htmlID, "data-disabled", "1")
if view.hasHtmlDisabled {
session.updateProperty(htmlID, "disabled", true)
}
if tabIndex >= 0 {
session.updateProperty(htmlID, "tabindex", -1)
}
if enabledClass != disabledClass {
session.updateProperty(htmlID, "class", disabledClass)
}
} else {
session.updateProperty(htmlID, "data-disabled", "0")
if view.hasHtmlDisabled {
session.removeProperty(htmlID, "disabled")
}
if tabIndex >= 0 {
session.updateProperty(htmlID, "tabindex", tabIndex)
}
if enabledClass != disabledClass {
session.updateProperty(htmlID, "class", enabledClass)
}
}
session.finishUpdateScript(htmlID)
updateInnerHTML(htmlID, session)
return
case Visibility:
@ -613,6 +650,8 @@ func viewPropertyChanged(view *viewData, tag string) {
case ZIndex, Order, TabSize:
if i, ok := intProperty(view, tag, session, 0); ok {
session.updateCSSProperty(htmlID, tag, strconv.Itoa(i))
} else {
session.updateCSSProperty(htmlID, tag, "")
}
return
@ -660,8 +699,11 @@ func viewPropertyChanged(view *viewData, tag string) {
}
if cssTag, ok := sizeProperties[tag]; ok {
size, _ := sizeProperty(view, tag, session)
session.updateCSSProperty(htmlID, cssTag, size.cssString("", session))
if size, ok := sizeProperty(view, tag, session); ok {
session.updateCSSProperty(htmlID, cssTag, size.cssString("", session))
} else {
session.updateCSSProperty(htmlID, cssTag, "")
}
return
}
@ -682,8 +724,11 @@ func viewPropertyChanged(view *viewData, tag string) {
}
if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" {
n, _ := enumProperty(view, tag, session, 0)
session.updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n])
if n, ok := enumProperty(view, tag, session, 0); ok {
session.updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n])
} else {
session.updateCSSProperty(htmlID, valuesData.cssTag, "")
}
return
}
@ -691,6 +736,8 @@ func viewPropertyChanged(view *viewData, tag string) {
if tag == floatTag {
if f, ok := floatTextProperty(view, floatTag, session, 0); ok {
session.updateCSSProperty(htmlID, floatTag, f)
} else {
session.updateCSSProperty(htmlID, floatTag, "")
}
return
}
@ -759,20 +806,22 @@ func (view *viewData) cssStyle(self View, builder cssBuilder) {
func (view *viewData) htmlProperties(self View, buffer *strings.Builder) {
view.created = true
if IsDisabled(self) {
buffer.WriteString(` data-disabled="1"`)
if view.hasHtmlDisabled {
buffer.WriteString(` disabled`)
}
} else {
buffer.WriteString(` data-disabled="0"`)
}
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('<')
@ -800,8 +849,6 @@ func viewHTML(view View, buffer *strings.Builder) {
buffer.WriteRune(' ')
view.htmlProperties(view, buffer)
buffer.WriteRune(' ')
view.htmlDisabledProperties(view, buffer)
if view.isNoResizeEvent() {
buffer.WriteString(` data-noresize="1" `)
@ -810,12 +857,10 @@ func viewHTML(view View, buffer *strings.Builder) {
}
if !disabled {
if value, ok := intProperty(view, TabIndex, view.Session(), -1); ok {
if tabIndex := GetTabIndex(view); tabIndex >= 0 {
buffer.WriteString(`tabindex="`)
buffer.WriteString(strconv.Itoa(value))
buffer.WriteString(strconv.Itoa(tabIndex))
buffer.WriteString(`" `)
} else if view.Focusable() {
buffer.WriteString(`tabindex="0" `)
}
}

View File

@ -13,8 +13,10 @@ type ViewStyle interface {
// Transition returns the transition animation of the property. Returns nil is there is no transition animation.
Transition(tag string) Animation
// Transitions returns the map of transition animations. The result is always non-nil.
Transitions() map[string]Animation
// SetTransition sets the transition animation for the property if "animation" argument is not nil, and
// removes the transition animation of the property if "animation" argument is nil.
// The "tag" argument is the property name.
@ -573,6 +575,9 @@ func supportedPropertyValue(value any) bool {
case []ViewShadow:
case []View:
case []any:
case []BackgroundElement:
case []BackgroundGradientPoint:
case []BackgroundGradientAngle:
case map[string]Animation:
default:
return false
@ -692,6 +697,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
for _, shadow := range value {
buffer.WriteString(indent2)
shadow.writeString(buffer, indent)
buffer.WriteRune(',')
}
buffer.WriteRune('\n')
buffer.WriteString(indent)
@ -701,7 +707,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
case []View:
switch len(value) {
case 0:
buffer.WriteString("[]\n")
buffer.WriteString("[]")
case 1:
writeViewStyle(value[0].Tag(), value[0], buffer, indent)
@ -740,6 +746,47 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
buffer.WriteString(" ]")
}
case []BackgroundElement:
switch len(value) {
case 0:
buffer.WriteString("[]\n")
case 1:
value[0].writeString(buffer, indent)
default:
buffer.WriteString("[\n")
indent2 := indent + "\t"
for _, element := range value {
buffer.WriteString(indent2)
element.writeString(buffer, indent2)
buffer.WriteString(",\n")
}
buffer.WriteString(indent)
buffer.WriteRune(']')
}
case []BackgroundGradientPoint:
buffer.WriteRune('"')
for i, point := range value {
if i > 0 {
buffer.WriteString(",")
}
buffer.WriteString(point.String())
}
buffer.WriteRune('"')
case []BackgroundGradientAngle:
buffer.WriteRune('"')
for i, point := range value {
if i > 0 {
buffer.WriteString(",")
}
buffer.WriteString(point.String())
}
buffer.WriteRune('"')
case map[string]Animation:
switch count := len(value); count {
case 0:

View File

@ -32,42 +32,52 @@ func (style *viewStyle) setRange(tag string, value any) bool {
}
func (style *viewStyle) setBackground(value any) bool {
background := []BackgroundElement{}
switch value := value.(type) {
case BackgroundElement:
style.properties[Background] = []BackgroundElement{value}
return true
background = []BackgroundElement{value}
case []BackgroundElement:
style.properties[Background] = value
return true
background = value
case []DataValue:
for _, el := range value {
if el.IsObject() {
if element := createBackground(el.Object()); element != nil {
background = append(background, element)
}
} else if obj := ParseDataText(el.Value()); obj != nil {
if element := createBackground(obj); element != nil {
background = append(background, element)
}
}
}
case DataObject:
if element := createBackground(value); element != nil {
style.properties[Background] = []BackgroundElement{element}
return true
background = []BackgroundElement{element}
}
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
background = []BackgroundElement{element}
}
}
}
if len(background) > 0 {
style.properties[Background] = background
return true
}
return false
}

View File

@ -11,12 +11,16 @@ type ParentView interface {
type ViewsContainer interface {
View
ParentView
// Append appends a view to the end of the list of a view children
Append(view View)
// Insert inserts a view to the "index" position in the list of a view children
Insert(view View, index int)
// Remove removes a view from the list of a view children and return it
RemoveView(index int) View
// ViewIndex returns the index of view, -1 overwise
ViewIndex(view View) int
}

View File

@ -16,7 +16,7 @@ type wasmBridge struct {
closeEvent chan DataObject
}
func createWasmBridge(close chan DataObject) webBridge {
func createWasmBridge(close chan DataObject) bridge {
bridge := new(wasmBridge)
bridge.answerID = 1
bridge.answer = make(map[int]chan DataObject)
@ -102,10 +102,6 @@ func (bridge *wasmBridge) close() {
bridge.closeEvent <- NewDataObject("close")
}
func (bridge *wasmBridge) readMessage() (string, bool) {
return "", false
}
func (bridge *wasmBridge) writeMessage(script string) bool {
if ProtocolInDebugLog {
DebugLog("Run script:")
@ -118,21 +114,24 @@ func (bridge *wasmBridge) writeMessage(script string) bool {
return true
}
func (bridge *wasmBridge) addAnimationCSS(css string) {
func (bridge *wasmBridge) prepareCSS(css string) string {
css = strings.ReplaceAll(css, `\t`, "\t")
css = strings.ReplaceAll(css, `\n`, "\n")
css = strings.ReplaceAll(css, `\'`, "'")
css = strings.ReplaceAll(css, `\"`, "\"")
css = strings.ReplaceAll(css, `\\`, "\\")
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
content := styles.Get("textContent").String()
styles.Set("textContent", content+"\n"+css)
return css
}
func (bridge *wasmBridge) clearAnimation() {
func (bridge *wasmBridge) appendAnimationCSS(css string) {
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
styles.Set("textContent", "")
content := styles.Get("textContent").String()
styles.Set("textContent", content+"\n"+bridge.prepareCSS(css))
}
func (bridge *wasmBridge) setAnimationCSS(css string) {
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
styles.Set("textContent", bridge.prepareCSS(css))
}
func (bridge *wasmBridge) canvasStart(htmlID string) {
@ -276,3 +275,6 @@ func (bridge *wasmBridge) answerReceived(answer DataObject) {
func (bridge *wasmBridge) remoteAddr() string {
return "localhost"
}
func (bridge *wasmBridge) sendResponse() {
}

View File

@ -12,16 +12,30 @@ import (
"github.com/gorilla/websocket"
)
type webBridge struct {
answer map[int]chan DataObject
answerID int
answerMutex sync.Mutex
writeMutex sync.Mutex
closed bool
canvasBuffer strings.Builder
canvasVarNumber int
updateScripts map[string]*strings.Builder
writeMessage func(string) bool
callFuncImmediately func(funcName string, args ...any) bool
}
type wsBridge struct {
conn *websocket.Conn
answer map[int]chan DataObject
answerID int
answerMutex sync.Mutex
closed bool
buffer strings.Builder
canvasBuffer strings.Builder
canvasVarNumber int
updateScripts map[string]*strings.Builder
webBridge
conn *websocket.Conn
}
type httpBridge struct {
webBridge
responseBuffer strings.Builder
response chan string
remoteAddress string
//conn *websocket.Conn
}
type canvasVar struct {
@ -33,7 +47,7 @@ var upgrader = websocket.Upgrader{
WriteBufferSize: 8096,
}
func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
func createSocketBridge(w http.ResponseWriter, req *http.Request) *wsBridge {
conn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
ErrorLog(err.Error())
@ -41,41 +55,84 @@ func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
}
bridge := new(wsBridge)
bridge.answerID = 1
bridge.answer = make(map[int]chan DataObject)
bridge.initBridge()
bridge.conn = conn
bridge.closed = false
bridge.updateScripts = map[string]*strings.Builder{}
bridge.writeMessage = func(script string) bool {
if ProtocolInDebugLog {
DebugLog("🖥️ <- " + script)
}
if bridge.conn == nil {
ErrorLog("No connection")
return false
}
bridge.writeMutex.Lock()
err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script))
bridge.writeMutex.Unlock()
if err != nil {
ErrorLog(err.Error())
return false
}
return true
}
bridge.callFuncImmediately = bridge.callFunc
return bridge
}
func (bridge *wsBridge) close() {
bridge.closed = true
bridge.conn.Close()
func createHttpBridge(req *http.Request) *httpBridge {
bridge := new(httpBridge)
bridge.initBridge()
bridge.response = make(chan string, 10)
bridge.writeMessage = func(script string) bool {
if script != "" {
if ProtocolInDebugLog {
DebugLog(script)
}
if bridge.responseBuffer.Len() > 0 {
bridge.responseBuffer.WriteRune('\n')
}
bridge.responseBuffer.WriteString(script)
}
return true
}
bridge.callFuncImmediately = bridge.callImmediately
bridge.remoteAddress = req.RemoteAddr
return bridge
}
func (bridge *wsBridge) startUpdateScript(htmlID string) bool {
func (bridge *webBridge) initBridge() {
bridge.answerID = 1
bridge.answer = make(map[int]chan DataObject)
bridge.closed = false
bridge.updateScripts = map[string]*strings.Builder{}
}
func (bridge *webBridge) startUpdateScript(htmlID string) bool {
if _, ok := bridge.updateScripts[htmlID]; ok {
return false
}
buffer := allocStringBuilder()
bridge.updateScripts[htmlID] = buffer
buffer.WriteString("var element = document.getElementById('")
buffer.WriteString("{\nlet element = document.getElementById('")
buffer.WriteString(htmlID)
buffer.WriteString("');\nif (element) {\n")
return true
}
func (bridge *wsBridge) finishUpdateScript(htmlID string) {
func (bridge *webBridge) finishUpdateScript(htmlID string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok {
buffer.WriteString("scanElementsSize();\n}\n")
buffer.WriteString("scanElementsSize();\n}\n}\n")
bridge.writeMessage(buffer.String())
freeStringBuilder(buffer)
delete(bridge.updateScripts, htmlID)
}
}
func (bridge *wsBridge) argToString(arg any) (string, bool) {
func (bridge *webBridge) argToString(arg any) (string, bool) {
switch arg := arg.(type) {
case string:
arg = strings.ReplaceAll(arg, "\\", `\\`)
@ -150,43 +207,44 @@ func (bridge *wsBridge) argToString(arg any) (string, bool) {
return "", false
}
func (bridge *wsBridge) callFunc(funcName string, args ...any) bool {
bridge.buffer.Reset()
bridge.buffer.WriteString(funcName)
bridge.buffer.WriteRune('(')
func (bridge *webBridge) callFuncScript(funcName string, args ...any) (string, bool) {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(funcName)
buffer.WriteRune('(')
for i, arg := range args {
argText, ok := bridge.argToString(arg)
if !ok {
return false
return "", false
}
if i > 0 {
bridge.buffer.WriteString(", ")
buffer.WriteString(", ")
}
bridge.buffer.WriteString(argText)
buffer.WriteString(argText)
}
bridge.buffer.WriteString(");")
buffer.WriteString(");")
funcText := bridge.buffer.String()
if ProtocolInDebugLog {
DebugLog("Run func: " + funcText)
}
if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(funcText)); err != nil {
ErrorLog(err.Error())
return false
}
return true
return buffer.String(), true
}
func (bridge *wsBridge) updateInnerHTML(htmlID, html string) {
func (bridge *webBridge) callFunc(funcName string, args ...any) bool {
if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
return bridge.writeMessage(funcText)
}
return false
}
func (bridge *webBridge) updateInnerHTML(htmlID, html string) {
bridge.callFunc("updateInnerHTML", htmlID, html)
}
func (bridge *wsBridge) appendToInnerHTML(htmlID, html string) {
func (bridge *webBridge) appendToInnerHTML(htmlID, html string) {
bridge.callFunc("appendToInnerHTML", htmlID, html)
}
func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
func (bridge *webBridge) updateCSSProperty(htmlID, property, value string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok {
buffer.WriteString(`element.style['`)
buffer.WriteString(property)
@ -198,7 +256,7 @@ func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
}
}
func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
func (bridge *webBridge) updateProperty(htmlID, property string, value any) {
if buffer, ok := bridge.updateScripts[htmlID]; ok {
if val, ok := bridge.argToString(value); ok {
buffer.WriteString(`element.setAttribute('`)
@ -212,7 +270,7 @@ func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
}
}
func (bridge *wsBridge) removeProperty(htmlID, property string) {
func (bridge *webBridge) removeProperty(htmlID, property string) {
if buffer, ok := bridge.updateScripts[htmlID]; ok {
buffer.WriteString(`if (element.hasAttribute('`)
buffer.WriteString(property)
@ -224,28 +282,34 @@ func (bridge *wsBridge) removeProperty(htmlID, property string) {
}
}
func (bridge *wsBridge) addAnimationCSS(css string) {
bridge.writeMessage(`var styles = document.getElementById('ruiAnimations');
if (styles) {
styles.textContent += '` + css + `';
func (bridge *webBridge) appendAnimationCSS(css string) {
//bridge.callFunc("appendAnimationCSS", css)
bridge.writeMessage(`{
let styles = document.getElementById('ruiAnimations');
if (styles) {
styles.textContent += '` + css + `';
}
}`)
}
func (bridge *wsBridge) clearAnimation() {
bridge.writeMessage(`var styles = document.getElementById('ruiAnimations');
if (styles) {
styles.textContent = '';
func (bridge *webBridge) setAnimationCSS(css string) {
//bridge.callFunc("setAnimationCSS", css)
bridge.writeMessage(`{
let styles = document.getElementById('ruiAnimations');
if (styles) {
styles.textContent = '` + css + `';
}
}`)
}
func (bridge *wsBridge) canvasStart(htmlID string) {
func (bridge *webBridge) canvasStart(htmlID string) {
bridge.canvasBuffer.Reset()
bridge.canvasBuffer.WriteString(`const ctx = getCanvasContext('`)
bridge.canvasBuffer.WriteString("{\nconst ctx = getCanvasContext('")
bridge.canvasBuffer.WriteString(htmlID)
bridge.canvasBuffer.WriteString(`');`)
}
func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
func (bridge *webBridge) callCanvasFunc(funcName string, args ...any) {
bridge.canvasBuffer.WriteString("\nctx.")
bridge.canvasBuffer.WriteString(funcName)
bridge.canvasBuffer.WriteRune('(')
@ -259,7 +323,7 @@ func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
bridge.canvasBuffer.WriteString(");")
}
func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
func (bridge *webBridge) updateCanvasProperty(property string, value any) {
bridge.canvasBuffer.WriteString("\nctx.")
bridge.canvasBuffer.WriteString(property)
bridge.canvasBuffer.WriteString(" = ")
@ -268,10 +332,10 @@ func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
bridge.canvasBuffer.WriteString(";")
}
func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
func (bridge *webBridge) createCanvasVar(funcName string, args ...any) any {
bridge.canvasVarNumber++
result := canvasVar{name: fmt.Sprintf("v%d", bridge.canvasVarNumber)}
bridge.canvasBuffer.WriteString("\nvar ")
bridge.canvasBuffer.WriteString("\nlet ")
bridge.canvasBuffer.WriteString(result.name)
bridge.canvasBuffer.WriteString(" = ctx.")
bridge.canvasBuffer.WriteString(funcName)
@ -287,7 +351,7 @@ func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
return result
}
func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
func (bridge *webBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
varName, ok := v.(canvasVar)
if !ok {
return
@ -307,7 +371,7 @@ func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
bridge.canvasBuffer.WriteString(");")
}
func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
func (bridge *webBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
bridge.canvasBuffer.WriteString("\nimg = images.get('")
bridge.canvasBuffer.WriteString(url)
@ -328,51 +392,12 @@ func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcNam
bridge.canvasBuffer.WriteString(");\n}")
}
func (bridge *wsBridge) canvasFinish() {
bridge.canvasBuffer.WriteString("\n")
script := bridge.canvasBuffer.String()
if ProtocolInDebugLog {
DebugLog("Run script:")
DebugLog(script)
}
if bridge.conn == nil {
ErrorLog("No connection")
} else if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
ErrorLog(err.Error())
}
func (bridge *webBridge) canvasFinish() {
bridge.canvasBuffer.WriteString("\n}\n")
bridge.writeMessage(bridge.canvasBuffer.String())
}
func (bridge *wsBridge) readMessage() (string, bool) {
_, p, err := bridge.conn.ReadMessage()
if err != nil {
if !bridge.closed {
ErrorLog(err.Error())
}
return "", false
}
return string(p), true
}
func (bridge *wsBridge) writeMessage(script string) bool {
if ProtocolInDebugLog {
DebugLog("Run script:")
DebugLog(script)
}
if bridge.conn == nil {
ErrorLog("No connection")
return false
}
if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
ErrorLog(err.Error())
return false
}
return true
}
func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
result := TextMetrics{}
func (bridge *webBridge) remoteValue(funcName string, args ...any) (DataObject, bool) {
bridge.answerMutex.Lock()
answerID := bridge.answerID
bridge.answerID++
@ -381,36 +406,36 @@ func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics
answer := make(chan DataObject)
bridge.answer[answerID] = answer
if bridge.callFunc("canvasTextMetrics", answerID, htmlID, font, text) {
data := <-answer
result.Width = dataFloatProperty(data, "width")
funcArgs := append([]any{answerID}, args...)
var result DataObject = nil
ok := bridge.callFuncImmediately(funcName, funcArgs...)
if ok {
result = <-answer
}
close(answer)
delete(bridge.answer, answerID)
return result, true
}
func (bridge *webBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
result := TextMetrics{}
if data, ok := bridge.remoteValue("canvasTextMetrics", htmlID, font, text); ok {
result.Width = dataFloatProperty(data, "width")
}
return result
}
func (bridge *wsBridge) htmlPropertyValue(htmlID, name string) string {
bridge.answerMutex.Lock()
answerID := bridge.answerID
bridge.answerID++
bridge.answerMutex.Unlock()
answer := make(chan DataObject)
bridge.answer[answerID] = answer
if bridge.callFunc("getPropertyValue", answerID, htmlID, name) {
data := <-answer
func (bridge *webBridge) htmlPropertyValue(htmlID, name string) string {
if data, ok := bridge.remoteValue("getPropertyValue", htmlID, name); ok {
if value, ok := data.PropertyValue("value"); ok {
return value
}
}
delete(bridge.answer, answerID)
return ""
}
func (bridge *wsBridge) answerReceived(answer DataObject) {
func (bridge *webBridge) answerReceived(answer DataObject) {
if text, ok := answer.PropertyValue("answerID"); ok {
if id, err := strconv.Atoi(text); err == nil {
if chanel, ok := bridge.answer[id]; ok {
@ -427,6 +452,55 @@ func (bridge *wsBridge) answerReceived(answer DataObject) {
}
}
func (bridge *wsBridge) close() {
bridge.closed = true
defer bridge.conn.Close()
bridge.conn = nil
}
func (bridge *wsBridge) readMessage() (string, bool) {
_, p, err := bridge.conn.ReadMessage()
if err != nil {
if !bridge.closed {
ErrorLog(err.Error())
}
return "", false
}
return string(p), true
}
func (bridge *wsBridge) sendResponse() {
}
func (bridge *wsBridge) remoteAddr() string {
return bridge.conn.RemoteAddr().String()
}
func (bridge *httpBridge) close() {
bridge.closed = true
// TODO
}
func (bridge *httpBridge) callImmediately(funcName string, args ...any) bool {
if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
if ProtocolInDebugLog {
DebugLog("Run func: " + funcText)
}
bridge.response <- funcText
return true
}
return false
}
func (bridge *httpBridge) sendResponse() {
bridge.writeMutex.Lock()
text := bridge.responseBuffer.String()
bridge.responseBuffer.Reset()
bridge.writeMutex.Unlock()
bridge.response <- text
}
func (bridge *httpBridge) remoteAddr() string {
return bridge.remoteAddress
}