Merge pull request #4 from anoshenko/0.14

0.14
This commit is contained in:
Alexei Anoshenko 2024-04-29 12:28:54 +03:00 committed by GitHub
commit cbca1e7c87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 1335 additions and 591 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 # v0.13.0
* Added SetHotKey function to Session interface * 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) 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. 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 | | 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) 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 | | 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 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
ColumnLayout is a container that implements the ViewsContainer interface. ColumnLayout is a container that implements the ViewsContainer interface.

View File

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

View File

@ -17,7 +17,7 @@ type wasmApp struct {
params AppParams params AppParams
createContentFunc func(Session) SessionContent createContentFunc func(Session) SessionContent
session Session session Session
bridge webBridge bridge bridge
close chan DataObject close chan DataObject
} }
@ -25,6 +25,12 @@ func (app *wasmApp) Finish() {
app.session.close() app.session.close()
} }
func (app *wasmApp) Params() AppParams {
params := app.params
params.SocketAutoClose = 0
return params
}
func debugLog(text string) { func debugLog(text string) {
js.Global().Get("console").Call("log", text) 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": case "session-close":
app.close <- obj 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: 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 let socket
var socketUrl
function sendMessage(message) { function sendMessage(message) {
if (socket) { if (!socket) {
socket.send(message) 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() { function createSocket(onopen) {
socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://" let socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
socketUrl += document.location.hostname socketUrl += document.location.hostname
var port = document.location.port const port = document.location.port
if (port) { if (port) {
socketUrl += ":" + port socketUrl += ":" + port
} }
socketUrl += window.location.pathname + "ws" socketUrl += window.location.pathname + "ws"
socket = new WebSocket(socketUrl); socket = new WebSocket(socketUrl);
socket.onopen = socketOpen; socket.onopen = onopen;
socket.onclose = socketClose; socket.onclose = onSocketClose;
socket.onerror = socketError; socket.onerror = onSocketError;
socket.onmessage = function(event) { socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data); window.execScript ? window.execScript(event.data) : window.eval(event.data);
}; };
}; }
function socketOpen() { function closeSocket() {
sendMessage( sessionInfo() ); 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 + "}" ); sendMessage( "reconnect{session=" + sessionID + "}" );
} }
function socketReconnect() { function socketReconnect() {
if (!socket) { if (!socket) {
socket = new WebSocket(socketUrl); createSocket(onSocketReopen);
socket.onopen = socketReopen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
} }
} }
function socketClose(event) { function onSocketClose(event) {
console.log("socket closed") console.log("socket closed")
socket = null; socket = null;
if (!event.wasClean && windowFocus) { if (!event.wasClean && windowFocus) {
@ -53,15 +73,6 @@ function socketClose(event) {
} }
} }
function socketError(error) { function onSocketError(error) {
console.log(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 { textarea {
margin: 2px; margin: 2px;
padding: 1px; padding: 4px;
overflow: auto; overflow: auto;
font-size: inherit; font-size: inherit;
resize: none;
} }
ul:focus { ul:focus {

View File

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

View File

@ -17,6 +17,7 @@ var defaultThemeText string
// Application - app interface // Application - app interface
type Application interface { type Application interface {
Finish() Finish()
Params() AppParams
removeSession(id int) removeSession(id int)
} }
@ -24,23 +25,37 @@ type Application interface {
type AppParams struct { type AppParams struct {
// Title - title of the app window/tab // Title - title of the app window/tab
Title string Title string
// TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android) // TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android)
TitleColor Color TitleColor Color
// Icon - the icon file name // Icon - the icon file name
Icon string Icon string
// CertFile - path of a certificate for the server must be provided // CertFile - path of a certificate for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated. // 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 // 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. // of the server's certificate, any intermediates, and the CA's certificate.
CertFile string CertFile string
// KeyFile - path of a private key for the server must be provided // KeyFile - path of a private key for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated. // if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
KeyFile string KeyFile string
// Redirect80 - if true then the function of redirect from port 80 to 443 is created // Redirect80 - if true then the function of redirect from port 80 to 443 is created
Redirect80 bool 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> buffer.WriteString(`<head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>`) <title>`)
@ -67,11 +82,7 @@ func getStartPage(buffer *strings.Builder, params AppParams, addScripts string)
buffer.WriteString(appStyles) buffer.WriteString(appStyles)
buffer.WriteString(`</style> buffer.WriteString(`</style>
<style id="ruiAnimations"></style> <style id="ruiAnimations"></style>
<script> <script src="/script.js"></script>
`)
buffer.WriteString(defaultScripts)
buffer.WriteString(addScripts)
buffer.WriteString(`</script>
</head> </head>
<body id="body" onkeydown="keyDownEvent(this, event)"> <body id="body" onkeydown="keyDownEvent(this, event)">
<div class="ruiRoot" id="ruiRootView"></div> <div class="ruiRoot" id="ruiRootView"></div>

View File

@ -1,6 +1,9 @@
package rui package rui
import "strings" import (
"fmt"
"strings"
)
const ( const (
// NoRepeat is value of the Repeat property of an background image: // NoRepeat is value of the Repeat property of an background image:
@ -61,6 +64,8 @@ const (
// BackgroundElement describes the background element. // BackgroundElement describes the background element.
type BackgroundElement interface { type BackgroundElement interface {
Properties Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string cssStyle(session Session) string
Tag() string Tag() string
Clone() BackgroundElement Clone() BackgroundElement
@ -239,3 +244,20 @@ func (image *backgroundImage) cssStyle(session Session) string {
return "" 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() 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 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 { func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
value, ok := gradient.properties[Gradient] value, ok := gradient.properties[Gradient]
@ -370,6 +397,18 @@ func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
return buffer.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 { func (gradient *backgroundRadialGradient) Tag() string {
return "radial-gradient" return "radial-gradient"
} }
@ -610,3 +649,17 @@ func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
return buffer.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) { func (picker *colorPickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "ColorPicker" picker.tag = "ColorPicker"
picker.hasHtmlDisabled = true
picker.colorChangedListeners = []func(ColorPicker, Color, Color){} picker.colorChangedListeners = []func(ColorPicker, Color, Color){}
picker.properties[Padding] = Px(0) 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 { func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":

View File

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

View File

@ -40,6 +40,7 @@ func newDatePicker(session Session) View {
func (picker *datePickerData) init(session Session) { func (picker *datePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "DatePicker" picker.tag = "DatePicker"
picker.hasHtmlDisabled = true
picker.dateChangedListeners = []func(DatePicker, time.Time, time.Time){} 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 { func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":

View File

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

View File

@ -58,6 +58,7 @@ func newEditView(session Session) View {
func (edit *editViewData) init(session Session) { func (edit *editViewData) init(session Session) {
edit.viewData.init(session) edit.viewData.init(session)
edit.hasHtmlDisabled = true
edit.textChangeListeners = []func(EditView, string, string){} edit.textChangeListeners = []func(EditView, string, string){}
edit.tag = "EditView" 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) { func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
if GetEditViewType(edit) == MultiLineText { if GetEditViewType(edit) == MultiLineText {
buffer.WriteString(GetText(edit)) buffer.WriteString(GetText(edit))
@ -517,19 +511,30 @@ func GetHint(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0]) view = ViewByID(view, subviewID[0])
} }
session := view.Session()
text := ""
if view != nil { if view != nil {
if text, ok := stringProperty(view, Hint, view.Session()); ok { var ok bool
return text text, ok = stringProperty(view, Hint, view.Session())
} if !ok {
if value := valueFromStyle(view, Hint); value != nil { if value := valueFromStyle(view, Hint); value != nil {
if text, ok := value.(string); ok { if text, ok = value.(string); ok {
if text, ok = view.Session().resolveConstants(text); ok { if text, ok = session.resolveConstants(text); !ok {
return text 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 // 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) { func (picker *filePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "FilePicker" picker.tag = "FilePicker"
picker.hasHtmlDisabled = true
picker.files = []FileInfo{} picker.files = []FileInfo{}
picker.loader = map[int]func(FileInfo, []byte){} picker.loader = map[int]func(FileInfo, []byte){}
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){} 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 { func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "fileSelected": case "fileSelected":

4
go.mod
View File

@ -2,4 +2,6 @@ module github.com/anoshenko/rui
go 1.18 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.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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" "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 // GridLayout - grid-container of View
type GridLayout interface { type GridLayout interface {
ViewsContainer ViewsContainer

View File

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

View File

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

View File

@ -7,16 +7,22 @@ import (
const ( const (
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation // TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
TopDownOrientation = 0 TopDownOrientation = 0
// StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation // StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation
StartToEndOrientation = 1 StartToEndOrientation = 1
// BottomUpOrientation - subviews are arranged from bottom to top // BottomUpOrientation - subviews are arranged from bottom to top
BottomUpOrientation = 2 BottomUpOrientation = 2
// EndToStartOrientation - subviews are arranged from right to left // EndToStartOrientation - subviews are arranged from right to left
EndToStartOrientation = 3 EndToStartOrientation = 3
// ListWrapOff - subviews are scrolled and "true" if a new row/column starts // ListWrapOff - subviews are scrolled and "true" if a new row/column starts
ListWrapOff = 0 ListWrapOff = 0
// ListWrapOn - the new row/column starts at bottom/right // ListWrapOn - the new row/column starts at bottom/right
ListWrapOn = 1 ListWrapOn = 1
// ListWrapReverse - the new row/column starts at top/left // ListWrapReverse - the new row/column starts at top/left
ListWrapReverse = 2 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 "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. // The main listener format: func(ListView, int), where the second argument is the item index.
ListItemClickedEvent = "list-item-clicked" ListItemClickedEvent = "list-item-clicked"
// ListItemSelectedEvent is the constant for "list-item-selected" property tag. // ListItemSelectedEvent is the constant for "list-item-selected" property tag.
// The "list-item-selected" event occurs when a list item becomes selected. // 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. // The main listener format: func(ListView, int), where the second argument is the item index.
ListItemSelectedEvent = "list-item-selected" ListItemSelectedEvent = "list-item-selected"
// ListItemCheckedEvent is the constant for "list-item-checked" property tag. // 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 "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. // The main listener format: func(ListView, []int), where the second argument is the array of checked item indexes.
ListItemCheckedEvent = "list-item-checked" ListItemCheckedEvent = "list-item-checked"
// ListItemStyle is the constant for "list-item-style" property tag. // ListItemStyle is the constant for "list-item-style" property tag.
// The "list-item-style" string property defines the style of an unselected item // The "list-item-style" string property defines the style of an unselected item
ListItemStyle = "list-item-style" ListItemStyle = "list-item-style"
// CurrentStyle is the constant for "current-style" property tag. // 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. // The "current-style" string property defines the style of the selected item when the ListView is focused.
CurrentStyle = "current-style" CurrentStyle = "current-style"
// CurrentInactiveStyle is the constant for "current-inactive-style" property tag. // 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. // The "current-inactive-style" string property defines the style of the selected item when the ListView is unfocused.
CurrentInactiveStyle = "current-inactive-style" CurrentInactiveStyle = "current-inactive-style"
@ -589,7 +594,7 @@ func (listView *listViewData) getItemFrames() []Frame {
return listView.itemFrame return listView.itemFrame
} }
func (listView *listViewData) itemAlign(self View, buffer *strings.Builder) { func (listView *listViewData) itemAlign(buffer *strings.Builder) {
values := enumProperties[ItemHorizontalAlign].cssValues values := enumProperties[ItemHorizontalAlign].cssValues
if hAlign := GetListItemHorizontalAlign(listView); hAlign >= 0 && hAlign < len(values) { if hAlign := GetListItemHorizontalAlign(listView); hAlign >= 0 && hAlign < len(values) {
buffer.WriteString(" justify-items: ") 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 { if itemWidth := GetListItemWidth(listView); itemWidth.Type != Auto {
buffer.WriteString(` min-width: `) buffer.WriteString(` min-width: `)
buffer.WriteString(itemWidth.cssString("", listView.Session())) 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() session := listView.Session()
contentBuilder := allocStringBuilder() contentBuilder := allocStringBuilder()
defer freeStringBuilder(contentBuilder) defer freeStringBuilder(contentBuilder)
contentBuilder.WriteString(`<div style="display: grid;`) contentBuilder.WriteString(`<div style="display: grid;`)
listView.itemAlign(self, contentBuilder) listView.itemAlign(contentBuilder)
onDivBuilder := allocStringBuilder() onDivBuilder := allocStringBuilder()
defer freeStringBuilder(onDivBuilder) defer freeStringBuilder(onDivBuilder)
@ -681,7 +686,7 @@ func (listView *listViewData) getDivs(self View, checkbox, hCheckboxAlign, vChec
return onDivBuilder.String(), offDivBuilder.String(), contentBuilder.String() 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() itemStyleBuilder := allocStringBuilder()
defer freeStringBuilder(itemStyleBuilder) defer freeStringBuilder(itemStyleBuilder)
@ -760,15 +765,15 @@ func (listView *listViewData) currentInactiveStyle() string {
return listView.itemStyle(CurrentInactiveStyle, "ruiListItemSelected") 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() count := listView.adapter.ListSize()
listViewID := listView.htmlID() listViewID := listView.htmlID()
hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView) hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView)
vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView) vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView)
itemDiv := listView.checkboxItemDiv(self, checkbox, hCheckboxAlign, vCheckboxAlign) itemDiv := listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign)
onDiv, offDiv, contentDiv := listView.getDivs(self, checkbox, hCheckboxAlign, vCheckboxAlign) onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign)
current := GetCurrent(listView) current := GetCurrent(listView)
checkedItems := GetListViewCheckedItems(listView) checkedItems := GetListViewCheckedItems(listView)
@ -784,7 +789,7 @@ func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builde
buffer.WriteString(listView.currentInactiveStyle()) 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;`) 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) { if !listView.adapter.IsListItemEnabled(i) {
buffer.WriteString(`" data-disabled="1`) 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() count := listView.adapter.ListSize()
listViewID := listView.htmlID() 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;`) 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.itemAlign(itemStyleBuilder)
listView.itemSize(self, itemStyleBuilder) listView.itemSize(itemStyleBuilder)
itemStyleBuilder.WriteString(`" onclick="listItemClickEvent(this, event)"`) itemStyleBuilder.WriteString(`" onclick="listItemClickEvent(this, event)"`)
itemStyle := itemStyleBuilder.String() itemStyle := itemStyleBuilder.String()
@ -865,12 +870,12 @@ func (listView *listViewData) updateCheckboxItem(index int, checked bool) {
checkbox := GetListViewCheckbox(listView) checkbox := GetListViewCheckbox(listView)
hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView) hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView)
vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView) vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView)
onDiv, offDiv, contentDiv := listView.getDivs(listView, checkbox, hCheckboxAlign, vCheckboxAlign) onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign)
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
buffer.WriteString(listView.checkboxItemDiv(listView, checkbox, hCheckboxAlign, vCheckboxAlign)) buffer.WriteString(listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign))
if checked { if checked {
buffer.WriteString(onDiv) buffer.WriteString(onDiv)
} else { } else {
@ -1061,9 +1066,9 @@ func (listView *listViewData) htmlSubviews(self View, buffer *strings.Builder) {
checkbox := GetListViewCheckbox(listView) checkbox := GetListViewCheckbox(listView)
if checkbox == NoneCheckbox { if checkbox == NoneCheckbox {
listView.noneCheckboxSubviews(self, buffer) listView.noneCheckboxSubviews(buffer)
} else { } else {
listView.checkboxSubviews(self, buffer, checkbox) listView.checkboxSubviews(buffer, checkbox)
} }
buffer.WriteString(`</div>`) buffer.WriteString(`</div>`)

View File

@ -13,15 +13,18 @@ const (
// to control audio/video playback, including volume, seeking, and pause/resume playback. // to control audio/video playback, including volume, seeking, and pause/resume playback.
// Its default value is false. // Its default value is false.
Controls = "controls" Controls = "controls"
// Loop is the constant for the "loop" property tag. // Loop is the constant for the "loop" property tag.
// If the "loop" bool property is "true", the audio/video player will automatically seek back // 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. // to the start upon reaching the end of the audio/video.
// Its default value is false. // Its default value is false.
Loop = "loop" Loop = "loop"
// Muted is the constant for the "muted" property tag. // Muted is the constant for the "muted" property tag.
// The "muted" bool property indicates whether the audio/video will be initially silenced. // The "muted" bool property indicates whether the audio/video will be initially silenced.
// Its default value is false. // Its default value is false.
Muted = "muted" Muted = "muted"
// Preload is the constant for the "preload" property tag. // 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 "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: // 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. // 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. // The "abort-event" event fired when the resource was not fully loaded, but not as the result of an error.
AbortEvent = "abort-event" AbortEvent = "abort-event"
// CanPlayEvent is the constant for the "can-play-event" property tag. // 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 // 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. // loaded to play the media up to its end without having to stop for further buffering of content.
CanPlayEvent = "can-play-event" CanPlayEvent = "can-play-event"
// CanPlayThroughEvent is the constant for the "can-play-through-event" property tag. // 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 // 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. // to its end without stopping for content buffering.
CanPlayThroughEvent = "can-play-through-event" CanPlayThroughEvent = "can-play-through-event"
// CompleteEvent is the constant for the "complete-event" property tag. // CompleteEvent is the constant for the "complete-event" property tag.
// The "complete-event" event occurs when the rendering of an OfflineAudioContext is terminated. // The "complete-event" event occurs when the rendering of an OfflineAudioContext is terminated.
CompleteEvent = "complete-event" CompleteEvent = "complete-event"
// DurationChangedEvent is the constant for the "duration-changed-event" property tag. // DurationChangedEvent is the constant for the "duration-changed-event" property tag.
// The "duration-changed-event" event occurs when the duration attribute has been updated. // The "duration-changed-event" event occurs when the duration attribute has been updated.
DurationChangedEvent = "duration-changed-event" DurationChangedEvent = "duration-changed-event"
// EmptiedEvent is the constant for the "emptied-event" property tag. // 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 // 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. // (or partially loaded), and the HTMLMediaElement.load method is called to reload it.
EmptiedEvent = "emptied-event" EmptiedEvent = "emptied-event"
// EndedEvent is the constant for the "ended-event" property tag. // 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. // The "ended-event" event occurs when the playback has stopped because the end of the media was reached.
EndedEvent = "ended-event" EndedEvent = "ended-event"
// LoadedDataEvent is the constant for the "loaded-data-event" property tag. // 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. // The "loaded-data-event" event occurs when the first frame of the media has finished loading.
LoadedDataEvent = "loaded-data-event" LoadedDataEvent = "loaded-data-event"
// LoadedMetadataEvent is the constant for the "loaded-metadata-event" property tag. // LoadedMetadataEvent is the constant for the "loaded-metadata-event" property tag.
// The "loaded-metadata-event" event occurs when the metadata has been loaded. // The "loaded-metadata-event" event occurs when the metadata has been loaded.
LoadedMetadataEvent = "loaded-metadata-event" LoadedMetadataEvent = "loaded-metadata-event"
// LoadStartEvent is the constant for the "load-start-event" property tag. // 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. // The "load-start-event" event is fired when the browser has started to load a resource.
LoadStartEvent = "load-start-event" LoadStartEvent = "load-start-event"
// PauseEvent is the constant for the "pause-event" property tag. // PauseEvent is the constant for the "pause-event" property tag.
// The "pause-event" event occurs when the playback has been paused. // The "pause-event" event occurs when the playback has been paused.
PauseEvent = "pause-event" PauseEvent = "pause-event"
// PlayEvent is the constant for the "play-event" property tag. // PlayEvent is the constant for the "play-event" property tag.
// The "play-event" event occurs when the playback has begun. // The "play-event" event occurs when the playback has begun.
PlayEvent = "play-event" PlayEvent = "play-event"
// PlayingEvent is the constant for the "playing-event" property tag. // 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. // 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" PlayingEvent = "playing-event"
// ProgressEvent is the constant for the "progress-event" property tag. // ProgressEvent is the constant for the "progress-event" property tag.
// The "progress-event" event is fired periodically as the browser loads a resource. // The "progress-event" event is fired periodically as the browser loads a resource.
ProgressEvent = "progress-event" ProgressEvent = "progress-event"
// RateChangeEvent is the constant for the "rate-change-event" property tag. // RateChangeEvent is the constant for the "rate-change-event" property tag.
// The "rate-change-event" event occurs when the playback rate has changed. // The "rate-change-event" event occurs when the playback rate has changed.
RateChangedEvent = "rate-changed-event" RateChangedEvent = "rate-changed-event"
// SeekedEvent is the constant for the "seeked-event" property tag. // SeekedEvent is the constant for the "seeked-event" property tag.
// The "seeked-event" event occurs when a seek operation completed. // The "seeked-event" event occurs when a seek operation completed.
SeekedEvent = "seeked-event" SeekedEvent = "seeked-event"
// SeekingEvent is the constant for the "seeking-event" property tag. // SeekingEvent is the constant for the "seeking-event" property tag.
// The "seeking-event" event occurs when a seek operation began. // The "seeking-event" event occurs when a seek operation began.
SeekingEvent = "seeking-event" SeekingEvent = "seeking-event"
// StalledEvent is the constant for the "stalled-event" property tag. // 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. // The "stalled-event" event occurs when the user agent is trying to fetch media data, but data is unexpectedly not forthcoming.
StalledEvent = "stalled-event" StalledEvent = "stalled-event"
// SuspendEvent is the constant for the "suspend-event" property tag. // SuspendEvent is the constant for the "suspend-event" property tag.
// The "suspend-event" event occurs when the media data loading has been suspended. // The "suspend-event" event occurs when the media data loading has been suspended.
SuspendEvent = "suspend-event" SuspendEvent = "suspend-event"
// TimeUpdateEvent is the constant for the "time-update-event" property tag. // 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. // The "time-update-event" event occurs when the time indicated by the currentTime attribute has been updated.
TimeUpdateEvent = "time-update-event" TimeUpdateEvent = "time-update-event"
// VolumeChangedEvent is the constant for the "volume-change-event" property tag. // VolumeChangedEvent is the constant for the "volume-change-event" property tag.
// The "volume-change-event" event occurs when the volume has changed. // The "volume-change-event" event occurs when the volume has changed.
VolumeChangedEvent = "volume-changed-event" VolumeChangedEvent = "volume-changed-event"
// WaitingEvent is the constant for the "waiting-event" property tag. // 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 // The "waiting-event" event occurs when the playback has stopped because of a temporary lack of data
WaitingEvent = "waiting-event" WaitingEvent = "waiting-event"
// PlayerErrorEvent is the constant for the "player-error-event" property tag. // 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 // The "player-error-event" event is fired when the resource could not be loaded due to an error
// (for example, a network connectivity problem). // (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 - value of the view "preload" property: indicates that the audio/video should not be preloaded.
PreloadNone = 0 PreloadNone = 0
// PreloadMetadata - value of the view "preload" property: indicates that only audio/video metadata (e.g. length) is fetched. // PreloadMetadata - value of the view "preload" property: indicates that only audio/video metadata (e.g. length) is fetched.
PreloadMetadata = 1 PreloadMetadata = 1
// PreloadAuto - value of the view "preload" property: indicates that the whole audio file can be downloaded, // 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. // even if the user is not expected to use it.
PreloadAuto = 2 PreloadAuto = 2
// PlayerErrorUnknown - MediaPlayer error code: An unknown error. // PlayerErrorUnknown - MediaPlayer error code: An unknown error.
PlayerErrorUnknown = 0 PlayerErrorUnknown = 0
// PlayerErrorAborted - MediaPlayer error code: The fetching of the associated resource was aborted by the user's request. // PlayerErrorAborted - MediaPlayer error code: The fetching of the associated resource was aborted by the user's request.
PlayerErrorAborted = 1 PlayerErrorAborted = 1
// PlayerErrorNetwork - MediaPlayer error code: Some kind of network error occurred which prevented the media // PlayerErrorNetwork - MediaPlayer error code: Some kind of network error occurred which prevented the media
// from being successfully fetched, despite having previously been available. // from being successfully fetched, despite having previously been available.
PlayerErrorNetwork = 2 PlayerErrorNetwork = 2
// PlayerErrorDecode - MediaPlayer error code: Despite having previously been determined to be usable, // 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. // an error occurred while trying to decode the media resource, resulting in an error.
PlayerErrorDecode = 3 PlayerErrorDecode = 3
// PlayerErrorSourceNotSupported - MediaPlayer error code: The associated resource or media provider object has been found to be unsuitable. // PlayerErrorSourceNotSupported - MediaPlayer error code: The associated resource or media provider object has been found to be unsuitable.
PlayerErrorSourceNotSupported = 4 PlayerErrorSourceNotSupported = 4
) )
type MediaPlayer interface { type MediaPlayer interface {
View View
// Play attempts to begin playback of the media. // Play attempts to begin playback of the media.
Play() Play()
// Pause will pause playback of the media, if the media is already in a paused state this method will have no effect. // Pause will pause playback of the media, if the media is already in a paused state this method will have no effect.
Pause() Pause()
// SetCurrentTime sets the current playback time in seconds. // SetCurrentTime sets the current playback time in seconds.
SetCurrentTime(seconds float64) SetCurrentTime(seconds float64)
// CurrentTime returns the current playback time in seconds. // CurrentTime returns the current playback time in seconds.
CurrentTime() float64 CurrentTime() float64
// Duration returns the value indicating the total duration of the media in seconds. // Duration returns the value indicating the total duration of the media in seconds.
// If no media data is available, the returned value is NaN. // If no media data is available, the returned value is NaN.
Duration() float64 Duration() float64
// SetPlaybackRate sets the rate at which the media is being played back. This is used to implement user controls // 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 // 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. // the current rate, so a value of 1.0 indicates normal speed.
SetPlaybackRate(rate float64) SetPlaybackRate(rate float64)
// PlaybackRate returns the rate at which the media is being played back. // PlaybackRate returns the rate at which the media is being played back.
PlaybackRate() float64 PlaybackRate() float64
// SetVolume sets the audio volume, from 0.0 (silent) to 1.0 (loudest). // SetVolume sets the audio volume, from 0.0 (silent) to 1.0 (loudest).
SetVolume(volume float64) SetVolume(volume float64)
// Volume returns the audio volume, from 0.0 (silent) to 1.0 (loudest). // Volume returns the audio volume, from 0.0 (silent) to 1.0 (loudest).
Volume() float64 Volume() float64
// IsEnded function tells whether the media element is ended. // IsEnded function tells whether the media element is ended.
IsEnded() bool IsEnded() bool
// IsPaused function tells whether the media element is paused. // IsPaused function tells whether the media element is paused.
IsPaused() bool 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 is a number of the main pressed button, usually the left button or the un-initialized state
PrimaryMouseButton = 0 PrimaryMouseButton = 0
// AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button // AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button
// or the middle button (if present) // or the middle button (if present)
AuxiliaryMouseButton = 1 AuxiliaryMouseButton = 1
// SecondaryMouseButton is a number of the secondary pressed button, usually the right button // SecondaryMouseButton is a number of the secondary pressed button, usually the right button
SecondaryMouseButton = 2 SecondaryMouseButton = 2
// MouseButton4 is a number of the fourth button, typically the Browser Back button // MouseButton4 is a number of the fourth button, typically the Browser Back button
MouseButton4 = 3 MouseButton4 = 3
// MouseButton5 is a number of the fifth button, typically the Browser Forward button // MouseButton5 is a number of the fifth button, typically the Browser Forward button
MouseButton5 = 4 MouseButton5 = 4
// PrimaryMouseMask is the mask of the primary button (usually the left button) // PrimaryMouseMask is the mask of the primary button (usually the left button)
PrimaryMouseMask = 1 PrimaryMouseMask = 1
// SecondaryMouseMask is the mask of the secondary button (usually the right button) // SecondaryMouseMask is the mask of the secondary button (usually the right button)
SecondaryMouseMask = 2 SecondaryMouseMask = 2
// AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button) // AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button)
AuxiliaryMouseMask = 4 AuxiliaryMouseMask = 4
// MouseMask4 is the mask of the 4th button (typically the "Browser Back" button) // MouseMask4 is the mask of the 4th button (typically the "Browser Back" button)
MouseMask4 = 8 MouseMask4 = 8
//MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button) //MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button)
MouseMask5 = 16 MouseMask5 = 16
) )

View File

@ -7,17 +7,37 @@ import (
) )
const ( 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" NumberChangedEvent = "number-changed"
NumberPickerType = "number-picker-type"
NumberPickerMin = "number-picker-min" // NumberPickerType is the constant for the "number-picker-type" property tag.
NumberPickerMax = "number-picker-max" // The "number-picker-type" int property sets the mode of NumberPicker. It can take the following values:
NumberPickerStep = "number-picker-step" // * NumberEditor (0) - NumberPicker is presented by editor. Default value;
NumberPickerValue = "number-picker-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 ( const (
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor // NumberEditor - type of NumberPicker. NumberPicker is presented by editor
NumberEditor = 0 NumberEditor = 0
// NumberSlider - type of NumberPicker. NumberPicker is presented by slider // NumberSlider - type of NumberPicker. NumberPicker is presented by slider
NumberSlider = 1 NumberSlider = 1
) )
@ -47,6 +67,7 @@ func newNumberPicker(session Session) View {
func (picker *numberPickerData) init(session Session) { func (picker *numberPickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "NumberPicker" picker.tag = "NumberPicker"
picker.hasHtmlDisabled = true
picker.numberChangedListeners = []func(NumberPicker, float64, float64){} picker.numberChangedListeners = []func(NumberPicker, float64, float64){}
} }
@ -232,13 +253,6 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde
buffer.WriteString(` oninput="editViewInputEvent(this)"`) 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 { func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": 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. // The type of return value depends on the property. If the property is not set then nil is returned.
Get(tag string) any Get(tag string) any
getRaw(tag string) any getRaw(tag string) any
// Set sets the value (second argument) of the property with name defined by the first argument. // 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 // 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 // a description of the error is written to the log
Set(tag string, value any) bool Set(tag string, value any) bool
setRaw(tag string, value any) setRaw(tag string, value any)
// Remove removes the property with name defined by the argument // Remove removes the property with name defined by the argument
Remove(tag string) Remove(tag string)
// Clear removes all properties // Clear removes all properties
Clear() Clear()
// AllTags returns an array of the set properties // AllTags returns an array of the set properties
AllTags() []string AllTags() []string
} }
@ -68,6 +72,28 @@ func (properties *propertyList) AllTags() []string {
return tags 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) { func parseProperties(properties Properties, object DataObject) {
count := object.PropertyCount() count := object.PropertyCount()
for i := 0; i < count; i++ { for i := 0; i < count; i++ {

View File

@ -666,7 +666,8 @@ const (
// Resize is the constant for the "resize" property tag. // 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. // 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" Resize = "resize"
// UserSelect is the constant for the "user-select" property tag. // UserSelect is the constant for the "user-select" property tag.

View File

@ -331,6 +331,16 @@ var enumProperties = map[string]struct {
"justify-items", "justify-items",
[]string{"start", "end", "center", "stretch"}, []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: { GridAutoFlow: {
[]string{"row", "column", "row-dense", "column-dense"}, []string{"row", "column", "row-dense", "column-dense"},
GridAutoFlow, GridAutoFlow,

View File

@ -11,22 +11,23 @@ const (
// The "side" int property determines which side of the container is used to resize. // 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) // The value of property is or-combination of TopSide (1), RightSide (2), BottomSide (4), and LeftSide (8)
Side = "side" Side = "side"
// ResizeBorderWidth is the constant for the "resize-border-width" property tag. // ResizeBorderWidth is the constant for the "resize-border-width" property tag.
// The "ResizeBorderWidth" SizeUnit property determines the width of the resizing border // The "ResizeBorderWidth" SizeUnit property determines the width of the resizing border
ResizeBorderWidth = "resize-border-width" 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 is value of the "side" property: the top side is used to resize
TopSide = 1 TopSide = 1
// RightSide is value of the "side" property: the right side is used to resize // RightSide is value of the "side" property: the right side is used to resize
RightSide = 2 RightSide = 2
// BottomSide is value of the "side" property: the bottom side is used to resize // BottomSide is value of the "side" property: the bottom side is used to resize
BottomSide = 4 BottomSide = 4
// LeftSide is value of the "side" property: the left side is used to resize // LeftSide is value of the "side" property: the left side is used to resize
LeftSide = 8 LeftSide = 8
// AllSides is value of the "side" property: all sides is used to resize // AllSides is value of the "side" property: all sides is used to resize
AllSides = TopSide | RightSide | BottomSide | LeftSide AllSides = TopSide | RightSide | BottomSide | LeftSide
) )

View File

@ -7,7 +7,7 @@ import (
"strings" "strings"
) )
type webBridge interface { type bridge interface {
startUpdateScript(htmlID string) bool startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string) finishUpdateScript(htmlID string)
callFunc(funcName string, args ...any) bool callFunc(funcName string, args ...any) bool
@ -16,10 +16,9 @@ type webBridge interface {
updateCSSProperty(htmlID, property, value string) updateCSSProperty(htmlID, property, value string)
updateProperty(htmlID, property string, value any) updateProperty(htmlID, property string, value any)
removeProperty(htmlID, property string) removeProperty(htmlID, property string)
readMessage() (string, bool) sendResponse()
writeMessage(text string) bool setAnimationCSS(css string)
addAnimationCSS(css string) appendAnimationCSS(css string)
clearAnimation()
canvasStart(htmlID string) canvasStart(htmlID string)
callCanvasFunc(funcName string, args ...any) callCanvasFunc(funcName string, args ...any)
callCanvasVarFunc(v any, 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. // Invoke SetHotKey(..., ..., nil) for remove hotkey function.
SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session)) 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 getCurrentTheme() Theme
registerAnimation(props []AnimatedProperty) string registerAnimation(props []AnimatedProperty) string
@ -124,7 +131,7 @@ type Session interface {
nextViewID() string nextViewID() string
styleProperty(styleTag, property string) any styleProperty(styleTag, property string) any
setBridge(events chan DataObject, bridge webBridge) setBridge(events chan DataObject, bridge bridge)
writeInitScript(writer *strings.Builder) writeInitScript(writer *strings.Builder)
callFunc(funcName string, args ...any) callFunc(funcName string, args ...any)
updateInnerHTML(htmlID, html string) updateInnerHTML(htmlID, html string)
@ -134,6 +141,7 @@ type Session interface {
removeProperty(htmlID, property string) removeProperty(htmlID, property string)
startUpdateScript(htmlID string) bool startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string) finishUpdateScript(htmlID string)
sendResponse()
addAnimationCSS(css string) addAnimationCSS(css string)
clearAnimation() clearAnimation()
canvasStart(htmlID string) canvasStart(htmlID string)
@ -145,7 +153,8 @@ type Session interface {
canvasFinish() canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string htmlPropertyValue(htmlID, name string) string
handleAnswer(data DataObject) addToEventsQueue(data DataObject)
handleAnswer(command string, data DataObject) bool
handleRootSize(data DataObject) handleRootSize(data DataObject)
handleResize(data DataObject) handleResize(data DataObject)
handleEvent(command string, data DataObject) handleEvent(command string, data DataObject)
@ -189,13 +198,16 @@ type sessionData struct {
ignoreUpdates bool ignoreUpdates bool
popups *popupManager popups *popupManager
images *imageManager images *imageManager
bridge webBridge bridge bridge
events chan DataObject events chan DataObject
animationCounter int animationCounter int
animationCSS string animationCSS string
updateScripts map[string]*strings.Builder updateScripts map[string]*strings.Builder
clientStorage map[string]string clientStorage map[string]string
hotkeys map[string]func(Session) 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 { 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.updateScripts = map[string]*strings.Builder{}
session.clientStorage = map[string]string{} session.clientStorage = map[string]string{}
session.hotkeys = map[string]func(Session){} session.hotkeys = map[string]func(Session){}
session.timers = map[int]func(Session){}
session.nextTimerID = 1
if customTheme != "" { if customTheme != "" {
if theme, ok := CreateThemeFromText(customTheme); ok { if theme, ok := CreateThemeFromText(customTheme); ok {
@ -237,7 +251,7 @@ func (session *sessionData) ID() int {
return session.sessionID 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.events = events
session.bridge = bridge session.bridge = bridge
} }
@ -330,23 +344,19 @@ func (session *sessionData) updateTooltipConstants() {
} }
func (session *sessionData) reload() { func (session *sessionData) reload() {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
css = strings.ReplaceAll(css, "\n", `\n`) session.bridge.callFunc("setStyles", css)
css = strings.ReplaceAll(css, "\t", `\t`)
buffer.WriteString(`document.querySelector('style').textContent = "`)
buffer.WriteString(css)
buffer.WriteString("\";\n")
if session.rootView != nil { if session.rootView != nil {
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(session.rootView, 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() 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) { func (session *sessionData) addAnimationCSS(css string) {
if session.bridge != nil { if session.bridge != nil {
session.bridge.addAnimationCSS(css) session.bridge.appendAnimationCSS(css)
} }
} }
func (session *sessionData) clearAnimation() { func (session *sessionData) clearAnimation() {
if session.bridge != nil { if session.bridge != nil {
session.bridge.clearAnimation() session.bridge.setAnimationCSS("")
} }
} }
@ -520,8 +536,27 @@ func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
return "" return ""
} }
func (session *sessionData) handleAnswer(data DataObject) { func (session *sessionData) handleAnswer(command string, data DataObject) bool {
session.bridge.answerReceived(data) 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) { func (session *sessionData) handleRootSize(data DataObject) {
@ -642,6 +677,22 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
case "session-resume": case "session-resume":
session.onResume() 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": case "root-size":
session.handleRootSize(data) session.handleRootSize(data)
@ -672,6 +723,8 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
ErrorLog(`"id" property not found. Event: ` + command) ErrorLog(`"id" property not found. Event: ` + command)
} }
} }
session.bridge.sendResponse()
} }
func (session *sessionData) hotKey(event KeyEvent) { func (session *sessionData) hotKey(event KeyEvent) {
@ -769,3 +822,25 @@ func (session *sessionData) RemoveAllClientItems() {
session.clientStorage = map[string]string{} session.clientStorage = map[string]string{}
session.bridge.callFunc("localStorageClear") 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 package rui
import "time"
// SessionStartListener is the listener interface of a session start event // SessionStartListener is the listener interface of a session start event
type SessionStartListener interface { type SessionStartListener interface {
OnStart(session Session) OnStart(session Session)
@ -50,13 +52,25 @@ func (session *sessionData) onFinish() {
func (session *sessionData) onPause() { func (session *sessionData) onPause() {
if session.content != nil { if session.content != nil {
session.pauseTime = time.Now().Unix()
if listener, ok := session.content.(SessionPauseListener); ok { if listener, ok := session.content.(SessionPauseListener); ok {
listener.OnPause(session) 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() { func (session *sessionData) onResume() {
session.pauseTime = 0
if session.content != nil { if session.content != nil {
if listener, ok := session.content.(SessionResumeListener); ok { if listener, ok := session.content.(SessionResumeListener); ok {
listener.OnResume(session) listener.OnResume(session)

View File

@ -22,10 +22,33 @@ const (
// StackLayout - list-container of View // StackLayout - list-container of View
type StackLayout interface { type StackLayout interface {
ViewsContainer ViewsContainer
// Peek returns the current (visible) View. If StackLayout is empty then it returns nil.
Peek() View 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 MoveToFront(view View) bool
// MoveToFrontByID makes the View current by viewID. Returns true if successful, false otherwise.
MoveToFrontByID(viewID string) bool 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()) 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 Pop(animation int, onPopFinished func(View)) bool
} }
@ -277,6 +300,10 @@ func (layout *stackLayoutData) RemoveView(index int) View {
return layout.viewsContainerData.RemoveView(index) 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()) { func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) {
if view == nil { if view == nil {
ErrorLog("StackLayout.Push(nil, ....) is forbidden") ErrorLog("StackLayout.Push(nil, ....) is forbidden")

View File

@ -40,6 +40,7 @@ func newTimePicker(session Session) View {
func (picker *timePickerData) init(session Session) { func (picker *timePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "TimePicker" picker.tag = "TimePicker"
picker.hasHtmlDisabled = true
picker.timeChangedListeners = []func(TimePicker, time.Time, time.Time){} 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 { func (picker *timePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":

View File

@ -8,9 +8,11 @@ const (
// VideoWidth is the constant for the "video-width" property tag of VideoPlayer. // 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. // The "video-width" float property defines the width of the video's display area in pixels.
VideoWidth = "video-width" VideoWidth = "video-width"
// VideoHeight is the constant for the "video-height" property tag of VideoPlayer. // 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. // The "video-height" float property defines the height of the video's display area in pixels.
VideoHeight = "video-height" VideoHeight = "video-height"
// Poster is the constant for the "poster" property tag of VideoPlayer. // 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. // 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, // 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 returns the current Session interface
Session() Session Session() Session
// Parent returns the parent view // Parent returns the parent view
Parent() View Parent() View
// Tag returns the tag of View interface // Tag returns the tag of View interface
Tag() string Tag() string
// ID returns the id of the view // ID returns the id of the view
ID() string ID() string
// Focusable returns true if the view receives the focus // Focusable returns true if the view receives the focus
Focusable() bool Focusable() bool
// Frame returns the location and size of the view in pixels // Frame returns the location and size of the view in pixels
Frame() Frame Frame() Frame
// Scroll returns the location size of the scrollable view in pixels // Scroll returns the location size of the scrollable view in pixels
Scroll() Frame Scroll() Frame
// SetAnimated sets the value (second argument) of the property with name defined by the first argument. // 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 // 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 // a description of the error is written to the log
SetAnimated(tag string, value any, animation Animation) bool SetAnimated(tag string, value any, animation Animation) bool
// SetChangeListener set the function to track the change of the View property // SetChangeListener set the function to track the change of the View property
SetChangeListener(tag string, listener func(View, string)) SetChangeListener(tag string, listener func(View, string))
// HasFocus returns 'true' if the view has focus // HasFocus returns 'true' if the view has focus
HasFocus() bool HasFocus() bool
@ -65,7 +74,6 @@ type View interface {
setParentID(parentID string) setParentID(parentID string)
htmlSubviews(self View, buffer *strings.Builder) htmlSubviews(self View, buffer *strings.Builder)
htmlProperties(self View, buffer *strings.Builder) htmlProperties(self View, buffer *strings.Builder)
htmlDisabledProperties(self View, buffer *strings.Builder)
cssStyle(self View, builder cssBuilder) cssStyle(self View, builder cssBuilder)
addToCSSStyle(addCSS map[string]string) addToCSSStyle(addCSS map[string]string)
@ -93,6 +101,7 @@ type viewData struct {
noResizeEvent bool noResizeEvent bool
created bool created bool
hasFocus bool hasFocus bool
hasHtmlDisabled bool
//animation map[string]AnimationEndListener //animation map[string]AnimationEndListener
} }
@ -135,6 +144,7 @@ func (view *viewData) init(session Session) {
view.singleTransition = map[string]Animation{} view.singleTransition = map[string]Animation{}
view.noResizeEvent = false view.noResizeEvent = false
view.created = false view.created = false
view.hasHtmlDisabled = false
} }
func (view *viewData) Session() Session { func (view *viewData) Session() Session {
@ -302,7 +312,6 @@ func (view *viewData) propertyChangedEvent(tag string) {
if listener, ok := view.changeListener[tag]; ok { if listener, ok := view.changeListener[tag]; ok {
listener(view, tag) listener(view, tag)
} }
} }
func (view *viewData) Set(tag string, value any) bool { func (view *viewData) Set(tag string, value any) bool {
@ -404,7 +413,35 @@ func viewPropertyChanged(view *viewData, tag string) {
switch tag { switch tag {
case Disabled: 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 return
case Visibility: case Visibility:
@ -613,6 +650,8 @@ func viewPropertyChanged(view *viewData, tag string) {
case ZIndex, Order, TabSize: case ZIndex, Order, TabSize:
if i, ok := intProperty(view, tag, session, 0); ok { if i, ok := intProperty(view, tag, session, 0); ok {
session.updateCSSProperty(htmlID, tag, strconv.Itoa(i)) session.updateCSSProperty(htmlID, tag, strconv.Itoa(i))
} else {
session.updateCSSProperty(htmlID, tag, "")
} }
return return
@ -660,8 +699,11 @@ func viewPropertyChanged(view *viewData, tag string) {
} }
if cssTag, ok := sizeProperties[tag]; ok { if cssTag, ok := sizeProperties[tag]; ok {
size, _ := sizeProperty(view, tag, session) if size, ok := sizeProperty(view, tag, session); ok {
session.updateCSSProperty(htmlID, cssTag, size.cssString("", session)) session.updateCSSProperty(htmlID, cssTag, size.cssString("", session))
} else {
session.updateCSSProperty(htmlID, cssTag, "")
}
return return
} }
@ -682,8 +724,11 @@ func viewPropertyChanged(view *viewData, tag string) {
} }
if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" { if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" {
n, _ := enumProperty(view, tag, session, 0) if n, ok := enumProperty(view, tag, session, 0); ok {
session.updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n]) session.updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n])
} else {
session.updateCSSProperty(htmlID, valuesData.cssTag, "")
}
return return
} }
@ -691,6 +736,8 @@ func viewPropertyChanged(view *viewData, tag string) {
if tag == floatTag { if tag == floatTag {
if f, ok := floatTextProperty(view, floatTag, session, 0); ok { if f, ok := floatTextProperty(view, floatTag, session, 0); ok {
session.updateCSSProperty(htmlID, floatTag, f) session.updateCSSProperty(htmlID, floatTag, f)
} else {
session.updateCSSProperty(htmlID, floatTag, "")
} }
return return
} }
@ -759,20 +806,22 @@ func (view *viewData) cssStyle(self View, builder cssBuilder) {
func (view *viewData) htmlProperties(self View, buffer *strings.Builder) { func (view *viewData) htmlProperties(self View, buffer *strings.Builder) {
view.created = true 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 { 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"`, 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)) 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) { func viewHTML(view View, buffer *strings.Builder) {
viewHTMLTag := view.htmlTag() viewHTMLTag := view.htmlTag()
buffer.WriteRune('<') buffer.WriteRune('<')
@ -800,8 +849,6 @@ func viewHTML(view View, buffer *strings.Builder) {
buffer.WriteRune(' ') buffer.WriteRune(' ')
view.htmlProperties(view, buffer) view.htmlProperties(view, buffer)
buffer.WriteRune(' ')
view.htmlDisabledProperties(view, buffer)
if view.isNoResizeEvent() { if view.isNoResizeEvent() {
buffer.WriteString(` data-noresize="1" `) buffer.WriteString(` data-noresize="1" `)
@ -810,12 +857,10 @@ func viewHTML(view View, buffer *strings.Builder) {
} }
if !disabled { if !disabled {
if value, ok := intProperty(view, TabIndex, view.Session(), -1); ok { if tabIndex := GetTabIndex(view); tabIndex >= 0 {
buffer.WriteString(`tabindex="`) buffer.WriteString(`tabindex="`)
buffer.WriteString(strconv.Itoa(value)) buffer.WriteString(strconv.Itoa(tabIndex))
buffer.WriteString(`" `) 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 returns the transition animation of the property. Returns nil is there is no transition animation.
Transition(tag string) Animation Transition(tag string) Animation
// Transitions returns the map of transition animations. The result is always non-nil. // Transitions returns the map of transition animations. The result is always non-nil.
Transitions() map[string]Animation Transitions() map[string]Animation
// SetTransition sets the transition animation for the property if "animation" argument is not nil, and // 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. // removes the transition animation of the property if "animation" argument is nil.
// The "tag" argument is the property name. // The "tag" argument is the property name.
@ -573,6 +575,9 @@ func supportedPropertyValue(value any) bool {
case []ViewShadow: case []ViewShadow:
case []View: case []View:
case []any: case []any:
case []BackgroundElement:
case []BackgroundGradientPoint:
case []BackgroundGradientAngle:
case map[string]Animation: case map[string]Animation:
default: default:
return false return false
@ -692,6 +697,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
for _, shadow := range value { for _, shadow := range value {
buffer.WriteString(indent2) buffer.WriteString(indent2)
shadow.writeString(buffer, indent) shadow.writeString(buffer, indent)
buffer.WriteRune(',')
} }
buffer.WriteRune('\n') buffer.WriteRune('\n')
buffer.WriteString(indent) buffer.WriteString(indent)
@ -701,7 +707,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
case []View: case []View:
switch len(value) { switch len(value) {
case 0: case 0:
buffer.WriteString("[]\n") buffer.WriteString("[]")
case 1: case 1:
writeViewStyle(value[0].Tag(), value[0], buffer, indent) 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(" ]") 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: case map[string]Animation:
switch count := len(value); count { switch count := len(value); count {
case 0: case 0:

View File

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

View File

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

View File

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

View File

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