mirror of https://github.com/anoshenko/rui.git
commit
cbca1e7c87
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -1,3 +1,14 @@
|
|||
# v0.14.0
|
||||
* Added the ability to work without creating a WebSocket. Added NoSocket property to AppParams.
|
||||
* Added SocketAutoClose property to AppParams.
|
||||
* Added the ability to run a timer on the client side. Added StartTimer and StopTimer methods to Session interface.
|
||||
* Added "cell-vertical-self-align", and "cell-horizontal-self-align" properties
|
||||
* Bug fixing
|
||||
|
||||
# v0.13.x
|
||||
* Added NewHandler function
|
||||
* Bug fixing
|
||||
|
||||
# v0.13.0
|
||||
|
||||
* Added SetHotKey function to Session interface
|
||||
|
|
16
README.md
16
README.md
|
@ -2465,9 +2465,10 @@ The SizeUnit value of type SizeInFraction can be either integer or fractional.
|
|||
The "grid-row-gap" and "grid-column-gap" SizeUnit properties (GridRowGap and GridColumnGap constants)
|
||||
allow you to set the distance between the rows and columns of the container, respectively. The default is 0px.
|
||||
|
||||
### "cell-vertical-align" property
|
||||
### "cell-vertical-align" and "cell-vertical-self-align" properties
|
||||
|
||||
The "cell-vertical-align" property (constant CellVerticalAlign) of type int sets the vertical alignment of children within the cell they are occupying. Valid values:
|
||||
The "cell-vertical-align" int property (constant CellVerticalAlign) sets the default vertical alignment of children
|
||||
within the cell they are occupying. Valid values:
|
||||
|
||||
| Value | Constant | Name | Alignment |
|
||||
|:-----:|--------------|-----------|---------------------|
|
||||
|
@ -2478,9 +2479,13 @@ The "cell-vertical-align" property (constant CellVerticalAlign) of type int sets
|
|||
|
||||
The default value is StretchAlign (3)
|
||||
|
||||
### "cell-horizontal-align" property
|
||||
The "cell-vertical-self-align" int property (constant CellVerticalAlign) sets the vertical alignment of children
|
||||
within the cell they are occupying. This property should be set not for the grid, but for the children.
|
||||
|
||||
The "cell-horizontal-align" property (constant CellHorizontalAlign) of type int sets the horizontal alignment of children within the occupied cell. Valid values:
|
||||
### "cell-horizontal-align" and "cell-horizontal-self-align" properties
|
||||
|
||||
The "cell-horizontal-align" int property (constant CellHorizontalSelfAlign) sets the horizontal alignment
|
||||
of children within the occupied cell. Valid values:
|
||||
|
||||
| Value | Constant | Name | Alignment |
|
||||
|:-----:|--------------|-----------|--------------------|
|
||||
|
@ -2491,6 +2496,9 @@ The "cell-horizontal-align" property (constant CellHorizontalAlign) of type int
|
|||
|
||||
The default value is StretchAlign (3)
|
||||
|
||||
The "cell-horizontal-self-align" int property (constant CellVerticalSelfAlign) sets the horizontal alignment of children
|
||||
within the cell they are occupying. This property should be set not for the grid, but for the children.
|
||||
|
||||
## ColumnLayout
|
||||
|
||||
ColumnLayout is a container that implements the ViewsContainer interface.
|
||||
|
|
193
appServer.go
193
appServer.go
|
@ -20,6 +20,9 @@ import (
|
|||
//go:embed app_socket.js
|
||||
var socketScripts string
|
||||
|
||||
//go:embed app_post.js
|
||||
var httpPostScripts string
|
||||
|
||||
func debugLog(text string) {
|
||||
log.Println("\033[34m" + text)
|
||||
}
|
||||
|
@ -28,11 +31,16 @@ func errorLog(text string) {
|
|||
log.Println("\033[31m" + text)
|
||||
}
|
||||
|
||||
type sessionInfo struct {
|
||||
session Session
|
||||
response chan string
|
||||
}
|
||||
|
||||
type application struct {
|
||||
server *http.Server
|
||||
params AppParams
|
||||
createContentFunc func(Session) SessionContent
|
||||
sessions map[int]Session
|
||||
sessions map[int]sessionInfo
|
||||
}
|
||||
|
||||
func (app *application) getStartPage() string {
|
||||
|
@ -40,14 +48,26 @@ func (app *application) getStartPage() string {
|
|||
defer freeStringBuilder(buffer)
|
||||
|
||||
buffer.WriteString("<!DOCTYPE html>\n<html>\n")
|
||||
getStartPage(buffer, app.params, socketScripts)
|
||||
getStartPage(buffer, app.params)
|
||||
buffer.WriteString("\n</html>")
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (app *application) Params() AppParams {
|
||||
params := app.params
|
||||
if params.NoSocket {
|
||||
params.SocketAutoClose = 0
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func (app *application) Finish() {
|
||||
for _, session := range app.sessions {
|
||||
session.close()
|
||||
session.session.close()
|
||||
if session.response != nil {
|
||||
close(session.response)
|
||||
session.response = nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
@ -69,7 +89,12 @@ func (app *application) nextSessionID() int {
|
|||
}
|
||||
|
||||
func (app *application) removeSession(id int) {
|
||||
delete(app.sessions, id)
|
||||
if info, ok := app.sessions[id]; ok {
|
||||
if info.response != nil {
|
||||
close(info.response)
|
||||
}
|
||||
delete(app.sessions, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
@ -79,6 +104,11 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
switch req.Method {
|
||||
case "POST":
|
||||
if req.URL.Path == "/" {
|
||||
app.postHandler(w, req)
|
||||
}
|
||||
|
||||
case "GET":
|
||||
switch req.URL.Path {
|
||||
case "/":
|
||||
|
@ -86,10 +116,20 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
io.WriteString(w, app.getStartPage())
|
||||
|
||||
case "/ws":
|
||||
if bridge := CreateSocketBridge(w, req); bridge != nil {
|
||||
if bridge := createSocketBridge(w, req); bridge != nil {
|
||||
go app.socketReader(bridge)
|
||||
}
|
||||
|
||||
case "/script.js":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if app.params.NoSocket {
|
||||
io.WriteString(w, httpPostScripts)
|
||||
} else {
|
||||
io.WriteString(w, socketScripts)
|
||||
}
|
||||
io.WriteString(w, "\n")
|
||||
io.WriteString(w, defaultScripts)
|
||||
|
||||
default:
|
||||
filename := req.URL.Path[1:]
|
||||
if size := len(filename); size > 0 && filename[size-1] == '/' {
|
||||
|
@ -104,7 +144,87 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func (app *application) socketReader(bridge webBridge) {
|
||||
func setSessionIDCookie(w http.ResponseWriter, sessionID int) {
|
||||
cookie := http.Cookie{
|
||||
Name: "session",
|
||||
Value: strconv.Itoa(sessionID),
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
func (app *application) postHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if reqBody, err := io.ReadAll(req.Body); err == nil {
|
||||
message := string(reqBody)
|
||||
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog(message)
|
||||
}
|
||||
|
||||
if obj := ParseDataText(message); obj != nil {
|
||||
var session Session = nil
|
||||
var response chan string = nil
|
||||
|
||||
if cookie, err := req.Cookie("session"); err == nil {
|
||||
sessionID, err := strconv.Atoi(cookie.Value)
|
||||
if err != nil {
|
||||
ErrorLog(err.Error())
|
||||
} else if info, ok := app.sessions[sessionID]; ok && info.response != nil {
|
||||
response = info.response
|
||||
session = info.session
|
||||
}
|
||||
}
|
||||
|
||||
command := obj.Tag()
|
||||
startSession := false
|
||||
|
||||
if session == nil || command == "startSession" {
|
||||
events := make(chan DataObject, 1024)
|
||||
bridge := createHttpBridge(req)
|
||||
response = bridge.response
|
||||
answer := ""
|
||||
session, answer = app.startSession(obj, events, bridge, response)
|
||||
|
||||
bridge.writeMessage(answer)
|
||||
session.onStart()
|
||||
if command == "session-resume" {
|
||||
session.onResume()
|
||||
}
|
||||
bridge.sendResponse()
|
||||
|
||||
setSessionIDCookie(w, session.ID())
|
||||
startSession = true
|
||||
|
||||
go sessionEventHandler(session, events, bridge)
|
||||
}
|
||||
|
||||
if !startSession {
|
||||
switch command {
|
||||
case "nop":
|
||||
session.sendResponse()
|
||||
|
||||
case "session-close":
|
||||
session.onFinish()
|
||||
session.App().removeSession(session.ID())
|
||||
return
|
||||
|
||||
default:
|
||||
if !session.handleAnswer(command, obj) {
|
||||
session.addToEventsQueue(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(w, <-response)
|
||||
for len(response) > 0 {
|
||||
io.WriteString(w, <-response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) socketReader(bridge *wsBridge) {
|
||||
var session Session
|
||||
events := make(chan DataObject, 1024)
|
||||
|
||||
|
@ -116,7 +236,7 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
}
|
||||
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog(message)
|
||||
DebugLog("🖥️ -> " + message)
|
||||
}
|
||||
|
||||
if obj := ParseDataText(message); obj != nil {
|
||||
|
@ -124,7 +244,7 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
switch command {
|
||||
case "startSession":
|
||||
answer := ""
|
||||
if session, answer = app.startSession(obj, events, bridge); session != nil {
|
||||
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
|
||||
if !bridge.writeMessage(answer) {
|
||||
return
|
||||
}
|
||||
|
@ -133,22 +253,18 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
}
|
||||
|
||||
case "reconnect":
|
||||
session = nil
|
||||
if sessionText, ok := obj.PropertyValue("session"); ok {
|
||||
if sessionID, err := strconv.Atoi(sessionText); err == nil {
|
||||
if session = app.sessions[sessionID]; session != nil {
|
||||
if info, ok := app.sessions[sessionID]; ok {
|
||||
session = info.session
|
||||
session.setBridge(events, bridge)
|
||||
answer := allocStringBuilder()
|
||||
defer freeStringBuilder(answer)
|
||||
|
||||
session.writeInitScript(answer)
|
||||
if !bridge.writeMessage(answer.String()) {
|
||||
return
|
||||
}
|
||||
session.onReconnect()
|
||||
go sessionEventHandler(session, events, bridge)
|
||||
return
|
||||
session.onReconnect()
|
||||
} else {
|
||||
DebugLogF("Session #%d not exists", sessionID)
|
||||
}
|
||||
DebugLogF("Session #%d not exists", sessionID)
|
||||
} else {
|
||||
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
|
||||
}
|
||||
|
@ -156,25 +272,31 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
ErrorLog(`"session" key not found`)
|
||||
}
|
||||
|
||||
bridge.writeMessage("restartSession();")
|
||||
|
||||
case "answer":
|
||||
session.handleAnswer(obj)
|
||||
|
||||
case "imageLoaded":
|
||||
session.imageManager().imageLoaded(obj, session)
|
||||
|
||||
case "imageError":
|
||||
session.imageManager().imageLoadError(obj, session)
|
||||
if session == nil {
|
||||
/* answer := ""
|
||||
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
|
||||
if !bridge.writeMessage(answer) {
|
||||
return
|
||||
}
|
||||
session.onStart()
|
||||
go sessionEventHandler(session, events, bridge)
|
||||
bridge.writeMessage("restartSession();")
|
||||
}
|
||||
*/
|
||||
bridge.writeMessage("reloadPage();")
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
events <- obj
|
||||
if !session.handleAnswer(command, obj) {
|
||||
events <- obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sessionEventHandler(session Session, events chan DataObject, bridge webBridge) {
|
||||
func sessionEventHandler(session Session, events chan DataObject, bridge bridge) {
|
||||
for {
|
||||
data := <-events
|
||||
|
||||
|
@ -194,7 +316,9 @@ func sessionEventHandler(session Session, events chan DataObject, bridge webBrid
|
|||
}
|
||||
}
|
||||
|
||||
func (app *application) startSession(params DataObject, events chan DataObject, bridge webBridge) (Session, string) {
|
||||
func (app *application) startSession(params DataObject, events chan DataObject,
|
||||
bridge bridge, response chan string) (Session, string) {
|
||||
|
||||
if app.createContentFunc == nil {
|
||||
return nil, ""
|
||||
}
|
||||
|
@ -205,7 +329,10 @@ func (app *application) startSession(params DataObject, events chan DataObject,
|
|||
return nil, ""
|
||||
}
|
||||
|
||||
app.sessions[session.ID()] = session
|
||||
app.sessions[session.ID()] = sessionInfo{
|
||||
session: session,
|
||||
response: response,
|
||||
}
|
||||
|
||||
answer := allocStringBuilder()
|
||||
defer freeStringBuilder(answer)
|
||||
|
@ -229,7 +356,7 @@ var apps = []*application{}
|
|||
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
|
||||
app := new(application)
|
||||
app.params = params
|
||||
app.sessions = map[int]Session{}
|
||||
app.sessions = map[int]sessionInfo{}
|
||||
app.createContentFunc = createContentFunc
|
||||
apps = append(apps, app)
|
||||
|
||||
|
|
21
appWasm.go
21
appWasm.go
|
@ -17,7 +17,7 @@ type wasmApp struct {
|
|||
params AppParams
|
||||
createContentFunc func(Session) SessionContent
|
||||
session Session
|
||||
bridge webBridge
|
||||
bridge bridge
|
||||
close chan DataObject
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,12 @@ func (app *wasmApp) Finish() {
|
|||
app.session.close()
|
||||
}
|
||||
|
||||
func (app *wasmApp) Params() AppParams {
|
||||
params := app.params
|
||||
params.SocketAutoClose = 0
|
||||
return params
|
||||
}
|
||||
|
||||
func debugLog(text string) {
|
||||
js.Global().Get("console").Call("log", text)
|
||||
}
|
||||
|
@ -44,17 +50,10 @@ func (app *wasmApp) handleMessage(this js.Value, args []js.Value) any {
|
|||
case "session-close":
|
||||
app.close <- obj
|
||||
|
||||
case "answer":
|
||||
app.session.handleAnswer(obj)
|
||||
|
||||
case "imageLoaded":
|
||||
app.session.imageManager().imageLoaded(obj, app.session)
|
||||
|
||||
case "imageError":
|
||||
app.session.imageManager().imageLoadError(obj, app.session)
|
||||
|
||||
default:
|
||||
app.session.handleEvent(command, obj)
|
||||
if !app.session.handleAnswer(command, obj) {
|
||||
app.session.handleEvent(command, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
}
|
460
app_scripts.js
460
app_scripts.js
File diff suppressed because it is too large
Load Diff
|
@ -1,51 +1,71 @@
|
|||
var socket
|
||||
var socketUrl
|
||||
let socket
|
||||
|
||||
function sendMessage(message) {
|
||||
if (socket) {
|
||||
socket.send(message)
|
||||
if (!socket) {
|
||||
createSocket(function() {
|
||||
sendMessage( "reconnect{session=" + sessionID + "}" );
|
||||
if (!windowFocus) {
|
||||
windowFocus = true;
|
||||
sendMessage( "session-resume{session=" + sessionID +"}" );
|
||||
}
|
||||
socket.send(message);
|
||||
});
|
||||
} else {
|
||||
socket.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
|
||||
function createSocket(onopen) {
|
||||
let socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
|
||||
socketUrl += document.location.hostname
|
||||
var port = document.location.port
|
||||
const port = document.location.port
|
||||
if (port) {
|
||||
socketUrl += ":" + port
|
||||
}
|
||||
socketUrl += window.location.pathname + "ws"
|
||||
|
||||
socket = new WebSocket(socketUrl);
|
||||
socket.onopen = socketOpen;
|
||||
socket.onclose = socketClose;
|
||||
socket.onerror = socketError;
|
||||
socket.onopen = onopen;
|
||||
socket.onclose = onSocketClose;
|
||||
socket.onerror = onSocketError;
|
||||
socket.onmessage = function(event) {
|
||||
window.execScript ? window.execScript(event.data) : window.eval(event.data);
|
||||
};
|
||||
};
|
||||
|
||||
function socketOpen() {
|
||||
sendMessage( sessionInfo() );
|
||||
}
|
||||
|
||||
function socketReopen() {
|
||||
function closeSocket() {
|
||||
if (socket) {
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = createSocket(function() {
|
||||
sendMessage( sessionInfo() );
|
||||
});
|
||||
|
||||
window.onfocus = function() {
|
||||
windowFocus = true
|
||||
if (!socket) {
|
||||
createSocket(function() {
|
||||
sendMessage( "reconnect{session=" + sessionID + "}" );
|
||||
sendMessage( "session-resume{session=" + sessionID +"}" );
|
||||
});
|
||||
} else {
|
||||
sendMessage( "session-resume{session=" + sessionID +"}" );
|
||||
}
|
||||
}
|
||||
|
||||
function onSocketReopen() {
|
||||
sendMessage( "reconnect{session=" + sessionID + "}" );
|
||||
}
|
||||
|
||||
function socketReconnect() {
|
||||
if (!socket) {
|
||||
socket = new WebSocket(socketUrl);
|
||||
socket.onopen = socketReopen;
|
||||
socket.onclose = socketClose;
|
||||
socket.onerror = socketError;
|
||||
socket.onmessage = function(event) {
|
||||
window.execScript ? window.execScript(event.data) : window.eval(event.data);
|
||||
};
|
||||
createSocket(onSocketReopen);
|
||||
}
|
||||
}
|
||||
|
||||
function socketClose(event) {
|
||||
function onSocketClose(event) {
|
||||
console.log("socket closed")
|
||||
socket = null;
|
||||
if (!event.wasClean && windowFocus) {
|
||||
|
@ -53,15 +73,6 @@ function socketClose(event) {
|
|||
}
|
||||
}
|
||||
|
||||
function socketError(error) {
|
||||
function onSocketError(error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
window.onfocus = function(event) {
|
||||
windowFocus = true
|
||||
if (!socket) {
|
||||
socketReconnect()
|
||||
} else {
|
||||
sendMessage( "session-resume{session=" + sessionID +"}" );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,9 +57,10 @@ button {
|
|||
|
||||
textarea {
|
||||
margin: 2px;
|
||||
padding: 1px;
|
||||
padding: 4px;
|
||||
overflow: auto;
|
||||
font-size: inherit;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
ul:focus {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
|
||||
window.onfocus = function(event) {
|
||||
window.onfocus = function() {
|
||||
windowFocus = true
|
||||
sendMessage( "session-resume{session=" + sessionID +"}" );
|
||||
}
|
||||
|
||||
function closeSocket() {
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ var defaultThemeText string
|
|||
// Application - app interface
|
||||
type Application interface {
|
||||
Finish()
|
||||
Params() AppParams
|
||||
removeSession(id int)
|
||||
}
|
||||
|
||||
|
@ -24,23 +25,37 @@ type Application interface {
|
|||
type AppParams struct {
|
||||
// Title - title of the app window/tab
|
||||
Title string
|
||||
|
||||
// TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android)
|
||||
TitleColor Color
|
||||
|
||||
// Icon - the icon file name
|
||||
Icon string
|
||||
|
||||
// CertFile - path of a certificate for the server must be provided
|
||||
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
|
||||
// If the certificate is signed by a certificate authority, the certFile should be the concatenation
|
||||
// of the server's certificate, any intermediates, and the CA's certificate.
|
||||
CertFile string
|
||||
|
||||
// KeyFile - path of a private key for the server must be provided
|
||||
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
|
||||
KeyFile string
|
||||
|
||||
// Redirect80 - if true then the function of redirect from port 80 to 443 is created
|
||||
Redirect80 bool
|
||||
|
||||
// NoSocket - if true then WebSockets will not be used and information exchange
|
||||
// between the client and the server will be carried out only via http.
|
||||
NoSocket bool
|
||||
|
||||
// SocketAutoClose - time in seconds after which the socket is automatically closed for an inactive session.
|
||||
// The countdown begins after the OnPause event arrives.
|
||||
// If the value of this property is less than or equal to 0 then the socket is not closed.
|
||||
SocketAutoClose int
|
||||
}
|
||||
|
||||
func getStartPage(buffer *strings.Builder, params AppParams, addScripts string) {
|
||||
func getStartPage(buffer *strings.Builder, params AppParams) {
|
||||
buffer.WriteString(`<head>
|
||||
<meta charset="utf-8">
|
||||
<title>`)
|
||||
|
@ -67,11 +82,7 @@ func getStartPage(buffer *strings.Builder, params AppParams, addScripts string)
|
|||
buffer.WriteString(appStyles)
|
||||
buffer.WriteString(`</style>
|
||||
<style id="ruiAnimations"></style>
|
||||
<script>
|
||||
`)
|
||||
buffer.WriteString(defaultScripts)
|
||||
buffer.WriteString(addScripts)
|
||||
buffer.WriteString(`</script>
|
||||
<script src="/script.js"></script>
|
||||
</head>
|
||||
<body id="body" onkeydown="keyDownEvent(this, event)">
|
||||
<div class="ruiRoot" id="ruiRootView"></div>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package rui
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// NoRepeat is value of the Repeat property of an background image:
|
||||
|
@ -61,6 +64,8 @@ const (
|
|||
// BackgroundElement describes the background element.
|
||||
type BackgroundElement interface {
|
||||
Properties
|
||||
fmt.Stringer
|
||||
stringWriter
|
||||
cssStyle(session Session) string
|
||||
Tag() string
|
||||
Clone() BackgroundElement
|
||||
|
@ -239,3 +244,20 @@ func (image *backgroundImage) cssStyle(session Session) string {
|
|||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (image *backgroundImage) writeString(buffer *strings.Builder, indent string) {
|
||||
image.writeToBuffer(buffer, indent, image.Tag(), []string{
|
||||
Source,
|
||||
Width,
|
||||
Height,
|
||||
ImageHorizontalAlign,
|
||||
ImageVerticalAlign,
|
||||
backgroundFit,
|
||||
Repeat,
|
||||
Attachment,
|
||||
})
|
||||
}
|
||||
|
||||
func (image *backgroundImage) String() string {
|
||||
return runStringWriter(image)
|
||||
}
|
||||
|
|
|
@ -336,3 +336,16 @@ func (gradient *backgroundConicGradient) cssStyle(session Session) string {
|
|||
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (gradient *backgroundConicGradient) writeString(buffer *strings.Builder, indent string) {
|
||||
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
|
||||
Gradient,
|
||||
CenterX,
|
||||
CenterY,
|
||||
Repeating,
|
||||
})
|
||||
}
|
||||
|
||||
func (gradient *backgroundConicGradient) String() string {
|
||||
return runStringWriter(gradient)
|
||||
}
|
||||
|
|
|
@ -224,6 +224,33 @@ func (point *BackgroundGradientPoint) color(session Session) (Color, bool) {
|
|||
return 0, false
|
||||
}
|
||||
|
||||
func (point *BackgroundGradientPoint) String() string {
|
||||
result := "black"
|
||||
if point.Color != nil {
|
||||
switch color := point.Color.(type) {
|
||||
case string:
|
||||
result = color
|
||||
|
||||
case Color:
|
||||
result = color.String()
|
||||
}
|
||||
}
|
||||
|
||||
if point.Pos != nil {
|
||||
switch value := point.Pos.(type) {
|
||||
case string:
|
||||
result += " " + value
|
||||
|
||||
case SizeUnit:
|
||||
if value.Type != Auto {
|
||||
result += " " + value.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
|
||||
|
||||
value, ok := gradient.properties[Gradient]
|
||||
|
@ -370,6 +397,18 @@ func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
|
|||
return buffer.String()
|
||||
}
|
||||
|
||||
func (gradient *backgroundLinearGradient) writeString(buffer *strings.Builder, indent string) {
|
||||
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
|
||||
Gradient,
|
||||
Repeating,
|
||||
Direction,
|
||||
})
|
||||
}
|
||||
|
||||
func (gradient *backgroundLinearGradient) String() string {
|
||||
return runStringWriter(gradient)
|
||||
}
|
||||
|
||||
func (gradient *backgroundRadialGradient) Tag() string {
|
||||
return "radial-gradient"
|
||||
}
|
||||
|
@ -610,3 +649,17 @@ func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
|
|||
|
||||
return buffer.String()
|
||||
}
|
||||
func (gradient *backgroundRadialGradient) writeString(buffer *strings.Builder, indent string) {
|
||||
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
|
||||
Gradient,
|
||||
CenterX,
|
||||
CenterY,
|
||||
Repeating,
|
||||
RadialGradientShape,
|
||||
RadialGradientRadius,
|
||||
})
|
||||
}
|
||||
|
||||
func (gradient *backgroundRadialGradient) String() string {
|
||||
return runStringWriter(gradient)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ func newColorPicker(session Session) View {
|
|||
func (picker *colorPickerData) init(session Session) {
|
||||
picker.viewData.init(session)
|
||||
picker.tag = "ColorPicker"
|
||||
picker.hasHtmlDisabled = true
|
||||
picker.colorChangedListeners = []func(ColorPicker, Color, Color){}
|
||||
picker.properties[Padding] = Px(0)
|
||||
}
|
||||
|
@ -153,13 +154,6 @@ func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder
|
|||
}
|
||||
}
|
||||
|
||||
func (picker *colorPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
if IsDisabled(self) {
|
||||
buffer.WriteString(` disabled`)
|
||||
}
|
||||
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||
}
|
||||
|
||||
func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||
switch command {
|
||||
case "textChanged":
|
||||
|
|
|
@ -188,10 +188,6 @@ func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Buil
|
|||
customView.superView.htmlProperties(customView.superView, buffer)
|
||||
}
|
||||
|
||||
func (customView *CustomViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
customView.superView.htmlDisabledProperties(customView.superView, buffer)
|
||||
}
|
||||
|
||||
func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) {
|
||||
customView.superView.cssStyle(customView.superView, builder)
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ func newDatePicker(session Session) View {
|
|||
func (picker *datePickerData) init(session Session) {
|
||||
picker.viewData.init(session)
|
||||
picker.tag = "DatePicker"
|
||||
picker.hasHtmlDisabled = true
|
||||
picker.dateChangedListeners = []func(DatePicker, time.Time, time.Time){}
|
||||
}
|
||||
|
||||
|
@ -303,13 +304,6 @@ func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder)
|
|||
}
|
||||
}
|
||||
|
||||
func (picker *datePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
if IsDisabled(self) {
|
||||
buffer.WriteString(` disabled`)
|
||||
}
|
||||
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||
}
|
||||
|
||||
func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||
switch command {
|
||||
case "textChanged":
|
||||
|
|
|
@ -39,6 +39,7 @@ func newDropDownList(session Session) View {
|
|||
func (list *dropDownListData) init(session Session) {
|
||||
list.viewData.init(session)
|
||||
list.tag = "DropDownList"
|
||||
list.hasHtmlDisabled = true
|
||||
list.items = []string{}
|
||||
list.disabledItems = []any{}
|
||||
list.dropDownListener = []func(DropDownList, int, int){}
|
||||
|
@ -370,13 +371,6 @@ func (list *dropDownListData) htmlProperties(self View, buffer *strings.Builder)
|
|||
buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`)
|
||||
}
|
||||
|
||||
func (list *dropDownListData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
list.viewData.htmlDisabledProperties(self, buffer)
|
||||
if IsDisabled(list) {
|
||||
buffer.WriteString(`disabled`)
|
||||
}
|
||||
}
|
||||
|
||||
func (list *dropDownListData) onSelectedItemChanged(number, old int) {
|
||||
for _, listener := range list.dropDownListener {
|
||||
listener(list, number, old)
|
||||
|
|
35
editView.go
35
editView.go
|
@ -58,6 +58,7 @@ func newEditView(session Session) View {
|
|||
|
||||
func (edit *editViewData) init(session Session) {
|
||||
edit.viewData.init(session)
|
||||
edit.hasHtmlDisabled = true
|
||||
edit.textChangeListeners = []func(EditView, string, string){}
|
||||
edit.tag = "EditView"
|
||||
}
|
||||
|
@ -466,13 +467,6 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
|
|||
}
|
||||
}
|
||||
|
||||
func (edit *editViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
if IsDisabled(self) {
|
||||
buffer.WriteString(` disabled`)
|
||||
}
|
||||
edit.viewData.htmlDisabledProperties(self, buffer)
|
||||
}
|
||||
|
||||
func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
|
||||
if GetEditViewType(edit) == MultiLineText {
|
||||
buffer.WriteString(GetText(edit))
|
||||
|
@ -517,19 +511,30 @@ func GetHint(view View, subviewID ...string) string {
|
|||
if len(subviewID) > 0 && subviewID[0] != "" {
|
||||
view = ViewByID(view, subviewID[0])
|
||||
}
|
||||
|
||||
session := view.Session()
|
||||
text := ""
|
||||
if view != nil {
|
||||
if text, ok := stringProperty(view, Hint, view.Session()); ok {
|
||||
return text
|
||||
}
|
||||
if value := valueFromStyle(view, Hint); value != nil {
|
||||
if text, ok := value.(string); ok {
|
||||
if text, ok = view.Session().resolveConstants(text); ok {
|
||||
return text
|
||||
var ok bool
|
||||
text, ok = stringProperty(view, Hint, view.Session())
|
||||
if !ok {
|
||||
if value := valueFromStyle(view, Hint); value != nil {
|
||||
if text, ok = value.(string); ok {
|
||||
if text, ok = session.resolveConstants(text); !ok {
|
||||
text = ""
|
||||
}
|
||||
} else {
|
||||
text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
if text != "" && !GetNotTranslate(view) {
|
||||
text, _ = session.GetString(text)
|
||||
}
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
// GetMaxLength returns a maximal length of EditView. If a maximal length is not limited then 0 is returned
|
||||
|
|
|
@ -83,6 +83,7 @@ func newFilePicker(session Session) View {
|
|||
func (picker *filePickerData) init(session Session) {
|
||||
picker.viewData.init(session)
|
||||
picker.tag = "FilePicker"
|
||||
picker.hasHtmlDisabled = true
|
||||
picker.files = []FileInfo{}
|
||||
picker.loader = map[int]func(FileInfo, []byte){}
|
||||
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
|
||||
|
@ -260,13 +261,6 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder)
|
|||
}
|
||||
}
|
||||
|
||||
func (picker *filePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
if IsDisabled(self) {
|
||||
buffer.WriteString(` disabled`)
|
||||
}
|
||||
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||
}
|
||||
|
||||
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||
switch command {
|
||||
case "fileSelected":
|
||||
|
|
4
go.mod
4
go.mod
|
@ -2,4 +2,6 @@ module github.com/anoshenko/rui
|
|||
|
||||
go 1.18
|
||||
|
||||
require github.com/gorilla/websocket v1.5.0
|
||||
require github.com/gorilla/websocket v1.5.1
|
||||
|
||||
require golang.org/x/net v0.17.0 // indirect
|
||||
|
|
6
go.sum
6
go.sum
|
@ -1,2 +1,4 @@
|
|||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
|
|
|
@ -5,6 +5,44 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// CellVerticalAlign is the constant for the "cell-vertical-align" property tag.
|
||||
// The "cell-vertical-align" int property sets the default vertical alignment
|
||||
// of GridLayout children within the cell they are occupying. Valid values:
|
||||
// * TopAlign (0) / "top"
|
||||
// * BottomAlign (1) / "bottom"
|
||||
// * CenterAlign (2) / "center", and
|
||||
// * StretchAlign (2) / "stretch"
|
||||
CellVerticalAlign = "cell-vertical-align"
|
||||
|
||||
// CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag.
|
||||
// The "cell-horizontal-align" int property sets the default horizontal alignment
|
||||
// of GridLayout children within the occupied cell. Valid values:
|
||||
// * LeftAlign (0) / "left"
|
||||
// * RightAlign (1) / "right"
|
||||
// * CenterAlign (2) / "center"
|
||||
// * StretchAlign (3) / "stretch"
|
||||
CellHorizontalAlign = "cell-horizontal-align"
|
||||
|
||||
// CellVerticalSelfAlign is the constant for the "cell-vertical-self-align" property tag.
|
||||
// The "cell-vertical-align" int property sets the vertical alignment of GridLayout children
|
||||
// within the cell they are occupying. The property is set for the child view of GridLayout. Valid values:
|
||||
// * TopAlign (0) / "top"
|
||||
// * BottomAlign (1) / "bottom"
|
||||
// * CenterAlign (2) / "center", and
|
||||
// * StretchAlign (2) / "stretch"
|
||||
CellVerticalSelfAlign = "cell-vertical-self-align"
|
||||
|
||||
// CellHorizontalSelfAlign is the constant for the "cell-horizontal-self-align" property tag.
|
||||
// The "cell-horizontal-self align" int property sets the horizontal alignment of GridLayout children
|
||||
// within the occupied cell. The property is set for the child view of GridLayout. Valid values:
|
||||
// * LeftAlign (0) / "left"
|
||||
// * RightAlign (1) / "right"
|
||||
// * CenterAlign (2) / "center"
|
||||
// * StretchAlign (3) / "stretch"
|
||||
CellHorizontalSelfAlign = "cell-horizontal-self-align"
|
||||
)
|
||||
|
||||
// GridLayout - grid-container of View
|
||||
type GridLayout interface {
|
||||
ViewsContainer
|
||||
|
|
5
image.go
5
image.go
|
@ -80,10 +80,11 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
|
|||
manager.images[url] = image
|
||||
|
||||
session.callFunc("loadImage", url)
|
||||
session.sendResponse()
|
||||
return image
|
||||
}
|
||||
|
||||
func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
|
||||
func (manager *imageManager) imageLoaded(obj DataObject) {
|
||||
if manager.images == nil {
|
||||
manager.images = make(map[string]*imageData)
|
||||
return
|
||||
|
@ -109,7 +110,7 @@ func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
|
|||
}
|
||||
}
|
||||
|
||||
func (manager *imageManager) imageLoadError(obj DataObject, session Session) {
|
||||
func (manager *imageManager) imageLoadError(obj DataObject) {
|
||||
if manager.images == nil {
|
||||
manager.images = make(map[string]*imageData)
|
||||
return
|
||||
|
|
|
@ -2,8 +2,13 @@ package rui
|
|||
|
||||
// ListAdapter - the list data source
|
||||
type ListAdapter interface {
|
||||
// ListSize returns the number of elements in the list
|
||||
ListSize() int
|
||||
|
||||
// ListItem creates a View of a list item at the given index
|
||||
ListItem(index int, session Session) View
|
||||
|
||||
// IsListItemEnabled returns the status (enabled/disabled) of a list item at the given index
|
||||
IsListItemEnabled(index int) bool
|
||||
}
|
||||
|
||||
|
|
|
@ -7,16 +7,22 @@ import (
|
|||
const (
|
||||
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
|
||||
TopDownOrientation = 0
|
||||
|
||||
// StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation
|
||||
StartToEndOrientation = 1
|
||||
|
||||
// BottomUpOrientation - subviews are arranged from bottom to top
|
||||
BottomUpOrientation = 2
|
||||
|
||||
// EndToStartOrientation - subviews are arranged from right to left
|
||||
EndToStartOrientation = 3
|
||||
|
||||
// ListWrapOff - subviews are scrolled and "true" if a new row/column starts
|
||||
ListWrapOff = 0
|
||||
|
||||
// ListWrapOn - the new row/column starts at bottom/right
|
||||
ListWrapOn = 1
|
||||
|
||||
// ListWrapReverse - the new row/column starts at top/left
|
||||
ListWrapReverse = 2
|
||||
)
|
||||
|
|
37
listView.go
37
listView.go
|
@ -11,20 +11,25 @@ const (
|
|||
// The "list-item-clicked" event occurs when the user clicks on an item in the list.
|
||||
// The main listener format: func(ListView, int), where the second argument is the item index.
|
||||
ListItemClickedEvent = "list-item-clicked"
|
||||
|
||||
// ListItemSelectedEvent is the constant for "list-item-selected" property tag.
|
||||
// The "list-item-selected" event occurs when a list item becomes selected.
|
||||
// The main listener format: func(ListView, int), where the second argument is the item index.
|
||||
ListItemSelectedEvent = "list-item-selected"
|
||||
|
||||
// ListItemCheckedEvent is the constant for "list-item-checked" property tag.
|
||||
// The "list-item-checked" event occurs when a list item checkbox becomes checked/unchecked.
|
||||
// The main listener format: func(ListView, []int), where the second argument is the array of checked item indexes.
|
||||
ListItemCheckedEvent = "list-item-checked"
|
||||
|
||||
// ListItemStyle is the constant for "list-item-style" property tag.
|
||||
// The "list-item-style" string property defines the style of an unselected item
|
||||
ListItemStyle = "list-item-style"
|
||||
|
||||
// CurrentStyle is the constant for "current-style" property tag.
|
||||
// The "current-style" string property defines the style of the selected item when the ListView is focused.
|
||||
CurrentStyle = "current-style"
|
||||
|
||||
// CurrentInactiveStyle is the constant for "current-inactive-style" property tag.
|
||||
// The "current-inactive-style" string property defines the style of the selected item when the ListView is unfocused.
|
||||
CurrentInactiveStyle = "current-inactive-style"
|
||||
|
@ -589,7 +594,7 @@ func (listView *listViewData) getItemFrames() []Frame {
|
|||
return listView.itemFrame
|
||||
}
|
||||
|
||||
func (listView *listViewData) itemAlign(self View, buffer *strings.Builder) {
|
||||
func (listView *listViewData) itemAlign(buffer *strings.Builder) {
|
||||
values := enumProperties[ItemHorizontalAlign].cssValues
|
||||
if hAlign := GetListItemHorizontalAlign(listView); hAlign >= 0 && hAlign < len(values) {
|
||||
buffer.WriteString(" justify-items: ")
|
||||
|
@ -605,7 +610,7 @@ func (listView *listViewData) itemAlign(self View, buffer *strings.Builder) {
|
|||
}
|
||||
}
|
||||
|
||||
func (listView *listViewData) itemSize(self View, buffer *strings.Builder) {
|
||||
func (listView *listViewData) itemSize(buffer *strings.Builder) {
|
||||
if itemWidth := GetListItemWidth(listView); itemWidth.Type != Auto {
|
||||
buffer.WriteString(` min-width: `)
|
||||
buffer.WriteString(itemWidth.cssString("", listView.Session()))
|
||||
|
@ -619,14 +624,14 @@ func (listView *listViewData) itemSize(self View, buffer *strings.Builder) {
|
|||
}
|
||||
}
|
||||
|
||||
func (listView *listViewData) getDivs(self View, checkbox, hCheckboxAlign, vCheckboxAlign int) (string, string, string) {
|
||||
func (listView *listViewData) getDivs(checkbox, hCheckboxAlign, vCheckboxAlign int) (string, string, string) {
|
||||
session := listView.Session()
|
||||
|
||||
contentBuilder := allocStringBuilder()
|
||||
defer freeStringBuilder(contentBuilder)
|
||||
|
||||
contentBuilder.WriteString(`<div style="display: grid;`)
|
||||
listView.itemAlign(self, contentBuilder)
|
||||
listView.itemAlign(contentBuilder)
|
||||
|
||||
onDivBuilder := allocStringBuilder()
|
||||
defer freeStringBuilder(onDivBuilder)
|
||||
|
@ -681,7 +686,7 @@ func (listView *listViewData) getDivs(self View, checkbox, hCheckboxAlign, vChec
|
|||
return onDivBuilder.String(), offDivBuilder.String(), contentBuilder.String()
|
||||
}
|
||||
|
||||
func (listView *listViewData) checkboxItemDiv(self View, checkbox, hCheckboxAlign, vCheckboxAlign int) string {
|
||||
func (listView *listViewData) checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign int) string {
|
||||
itemStyleBuilder := allocStringBuilder()
|
||||
defer freeStringBuilder(itemStyleBuilder)
|
||||
|
||||
|
@ -760,15 +765,15 @@ func (listView *listViewData) currentInactiveStyle() string {
|
|||
return listView.itemStyle(CurrentInactiveStyle, "ruiListItemSelected")
|
||||
}
|
||||
|
||||
func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builder, checkbox int) {
|
||||
func (listView *listViewData) checkboxSubviews(buffer *strings.Builder, checkbox int) {
|
||||
count := listView.adapter.ListSize()
|
||||
listViewID := listView.htmlID()
|
||||
|
||||
hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView)
|
||||
vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView)
|
||||
|
||||
itemDiv := listView.checkboxItemDiv(self, checkbox, hCheckboxAlign, vCheckboxAlign)
|
||||
onDiv, offDiv, contentDiv := listView.getDivs(self, checkbox, hCheckboxAlign, vCheckboxAlign)
|
||||
itemDiv := listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign)
|
||||
onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign)
|
||||
|
||||
current := GetCurrent(listView)
|
||||
checkedItems := GetListViewCheckedItems(listView)
|
||||
|
@ -784,7 +789,7 @@ func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builde
|
|||
buffer.WriteString(listView.currentInactiveStyle())
|
||||
}
|
||||
buffer.WriteString(`" onclick="listItemClickEvent(this, event)" data-left="0" data-top="0" data-width="0" data-height="0" style="display: grid; justify-items: stretch; align-items: stretch;`)
|
||||
listView.itemSize(self, buffer)
|
||||
listView.itemSize(buffer)
|
||||
if !listView.adapter.IsListItemEnabled(i) {
|
||||
buffer.WriteString(`" data-disabled="1`)
|
||||
}
|
||||
|
@ -815,7 +820,7 @@ func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builde
|
|||
}
|
||||
}
|
||||
|
||||
func (listView *listViewData) noneCheckboxSubviews(self View, buffer *strings.Builder) {
|
||||
func (listView *listViewData) noneCheckboxSubviews(buffer *strings.Builder) {
|
||||
count := listView.adapter.ListSize()
|
||||
listViewID := listView.htmlID()
|
||||
|
||||
|
@ -824,8 +829,8 @@ func (listView *listViewData) noneCheckboxSubviews(self View, buffer *strings.Bu
|
|||
|
||||
itemStyleBuilder.WriteString(`data-left="0" data-top="0" data-width="0" data-height="0" style="max-width: 100%; max-height: 100%; display: grid;`)
|
||||
|
||||
listView.itemAlign(self, itemStyleBuilder)
|
||||
listView.itemSize(self, itemStyleBuilder)
|
||||
listView.itemAlign(itemStyleBuilder)
|
||||
listView.itemSize(itemStyleBuilder)
|
||||
|
||||
itemStyleBuilder.WriteString(`" onclick="listItemClickEvent(this, event)"`)
|
||||
itemStyle := itemStyleBuilder.String()
|
||||
|
@ -865,12 +870,12 @@ func (listView *listViewData) updateCheckboxItem(index int, checked bool) {
|
|||
checkbox := GetListViewCheckbox(listView)
|
||||
hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView)
|
||||
vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView)
|
||||
onDiv, offDiv, contentDiv := listView.getDivs(listView, checkbox, hCheckboxAlign, vCheckboxAlign)
|
||||
onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign)
|
||||
|
||||
buffer := allocStringBuilder()
|
||||
defer freeStringBuilder(buffer)
|
||||
|
||||
buffer.WriteString(listView.checkboxItemDiv(listView, checkbox, hCheckboxAlign, vCheckboxAlign))
|
||||
buffer.WriteString(listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign))
|
||||
if checked {
|
||||
buffer.WriteString(onDiv)
|
||||
} else {
|
||||
|
@ -1061,9 +1066,9 @@ func (listView *listViewData) htmlSubviews(self View, buffer *strings.Builder) {
|
|||
|
||||
checkbox := GetListViewCheckbox(listView)
|
||||
if checkbox == NoneCheckbox {
|
||||
listView.noneCheckboxSubviews(self, buffer)
|
||||
listView.noneCheckboxSubviews(buffer)
|
||||
} else {
|
||||
listView.checkboxSubviews(self, buffer, checkbox)
|
||||
listView.checkboxSubviews(buffer, checkbox)
|
||||
}
|
||||
|
||||
buffer.WriteString(`</div>`)
|
||||
|
|
|
@ -13,15 +13,18 @@ const (
|
|||
// to control audio/video playback, including volume, seeking, and pause/resume playback.
|
||||
// Its default value is false.
|
||||
Controls = "controls"
|
||||
|
||||
// Loop is the constant for the "loop" property tag.
|
||||
// If the "loop" bool property is "true", the audio/video player will automatically seek back
|
||||
// to the start upon reaching the end of the audio/video.
|
||||
// Its default value is false.
|
||||
Loop = "loop"
|
||||
|
||||
// Muted is the constant for the "muted" property tag.
|
||||
// The "muted" bool property indicates whether the audio/video will be initially silenced.
|
||||
// Its default value is false.
|
||||
Muted = "muted"
|
||||
|
||||
// Preload is the constant for the "preload" property tag.
|
||||
// The "preload" int property is intended to provide a hint to the browser about what
|
||||
// the author thinks will lead to the best user experience. It may have one of the following values:
|
||||
|
@ -32,72 +35,94 @@ const (
|
|||
// AbortEvent is the constant for the "abort-event" property tag.
|
||||
// The "abort-event" event fired when the resource was not fully loaded, but not as the result of an error.
|
||||
AbortEvent = "abort-event"
|
||||
|
||||
// CanPlayEvent is the constant for the "can-play-event" property tag.
|
||||
// The "can-play-event" event occurs when the browser can play the media, but estimates that not enough data has been
|
||||
// loaded to play the media up to its end without having to stop for further buffering of content.
|
||||
CanPlayEvent = "can-play-event"
|
||||
|
||||
// CanPlayThroughEvent is the constant for the "can-play-through-event" property tag.
|
||||
// The "can-play-through-event" event occurs when the browser estimates it can play the media up
|
||||
// to its end without stopping for content buffering.
|
||||
CanPlayThroughEvent = "can-play-through-event"
|
||||
|
||||
// CompleteEvent is the constant for the "complete-event" property tag.
|
||||
// The "complete-event" event occurs when the rendering of an OfflineAudioContext is terminated.
|
||||
CompleteEvent = "complete-event"
|
||||
|
||||
// DurationChangedEvent is the constant for the "duration-changed-event" property tag.
|
||||
// The "duration-changed-event" event occurs when the duration attribute has been updated.
|
||||
DurationChangedEvent = "duration-changed-event"
|
||||
|
||||
// EmptiedEvent is the constant for the "emptied-event" property tag.
|
||||
// The "emptied-event" event occurs when the media has become empty; for example, this event is sent if the media has already been loaded
|
||||
// (or partially loaded), and the HTMLMediaElement.load method is called to reload it.
|
||||
EmptiedEvent = "emptied-event"
|
||||
|
||||
// EndedEvent is the constant for the "ended-event" property tag.
|
||||
// The "ended-event" event occurs when the playback has stopped because the end of the media was reached.
|
||||
EndedEvent = "ended-event"
|
||||
|
||||
// LoadedDataEvent is the constant for the "loaded-data-event" property tag.
|
||||
// The "loaded-data-event" event occurs when the first frame of the media has finished loading.
|
||||
LoadedDataEvent = "loaded-data-event"
|
||||
|
||||
// LoadedMetadataEvent is the constant for the "loaded-metadata-event" property tag.
|
||||
// The "loaded-metadata-event" event occurs when the metadata has been loaded.
|
||||
LoadedMetadataEvent = "loaded-metadata-event"
|
||||
|
||||
// LoadStartEvent is the constant for the "load-start-event" property tag.
|
||||
// The "load-start-event" event is fired when the browser has started to load a resource.
|
||||
LoadStartEvent = "load-start-event"
|
||||
|
||||
// PauseEvent is the constant for the "pause-event" property tag.
|
||||
// The "pause-event" event occurs when the playback has been paused.
|
||||
PauseEvent = "pause-event"
|
||||
|
||||
// PlayEvent is the constant for the "play-event" property tag.
|
||||
// The "play-event" event occurs when the playback has begun.
|
||||
PlayEvent = "play-event"
|
||||
|
||||
// PlayingEvent is the constant for the "playing-event" property tag.
|
||||
// The "playing-event" event occurs when the playback is ready to start after having been paused or delayed due to lack of data.
|
||||
PlayingEvent = "playing-event"
|
||||
|
||||
// ProgressEvent is the constant for the "progress-event" property tag.
|
||||
// The "progress-event" event is fired periodically as the browser loads a resource.
|
||||
ProgressEvent = "progress-event"
|
||||
|
||||
// RateChangeEvent is the constant for the "rate-change-event" property tag.
|
||||
// The "rate-change-event" event occurs when the playback rate has changed.
|
||||
RateChangedEvent = "rate-changed-event"
|
||||
|
||||
// SeekedEvent is the constant for the "seeked-event" property tag.
|
||||
// The "seeked-event" event occurs when a seek operation completed.
|
||||
SeekedEvent = "seeked-event"
|
||||
|
||||
// SeekingEvent is the constant for the "seeking-event" property tag.
|
||||
// The "seeking-event" event occurs when a seek operation began.
|
||||
SeekingEvent = "seeking-event"
|
||||
|
||||
// StalledEvent is the constant for the "stalled-event" property tag.
|
||||
// The "stalled-event" event occurs when the user agent is trying to fetch media data, but data is unexpectedly not forthcoming.
|
||||
StalledEvent = "stalled-event"
|
||||
|
||||
// SuspendEvent is the constant for the "suspend-event" property tag.
|
||||
// The "suspend-event" event occurs when the media data loading has been suspended.
|
||||
SuspendEvent = "suspend-event"
|
||||
|
||||
// TimeUpdateEvent is the constant for the "time-update-event" property tag.
|
||||
// The "time-update-event" event occurs when the time indicated by the currentTime attribute has been updated.
|
||||
TimeUpdateEvent = "time-update-event"
|
||||
|
||||
// VolumeChangedEvent is the constant for the "volume-change-event" property tag.
|
||||
// The "volume-change-event" event occurs when the volume has changed.
|
||||
VolumeChangedEvent = "volume-changed-event"
|
||||
|
||||
// WaitingEvent is the constant for the "waiting-event" property tag.
|
||||
// The "waiting-event" event occurs when the playback has stopped because of a temporary lack of data
|
||||
WaitingEvent = "waiting-event"
|
||||
|
||||
// PlayerErrorEvent is the constant for the "player-error-event" property tag.
|
||||
// The "player-error-event" event is fired when the resource could not be loaded due to an error
|
||||
// (for example, a network connectivity problem).
|
||||
|
@ -105,51 +130,68 @@ const (
|
|||
|
||||
// PreloadNone - value of the view "preload" property: indicates that the audio/video should not be preloaded.
|
||||
PreloadNone = 0
|
||||
|
||||
// PreloadMetadata - value of the view "preload" property: indicates that only audio/video metadata (e.g. length) is fetched.
|
||||
PreloadMetadata = 1
|
||||
|
||||
// PreloadAuto - value of the view "preload" property: indicates that the whole audio file can be downloaded,
|
||||
// even if the user is not expected to use it.
|
||||
PreloadAuto = 2
|
||||
|
||||
// PlayerErrorUnknown - MediaPlayer error code: An unknown error.
|
||||
PlayerErrorUnknown = 0
|
||||
|
||||
// PlayerErrorAborted - MediaPlayer error code: The fetching of the associated resource was aborted by the user's request.
|
||||
PlayerErrorAborted = 1
|
||||
|
||||
// PlayerErrorNetwork - MediaPlayer error code: Some kind of network error occurred which prevented the media
|
||||
// from being successfully fetched, despite having previously been available.
|
||||
PlayerErrorNetwork = 2
|
||||
|
||||
// PlayerErrorDecode - MediaPlayer error code: Despite having previously been determined to be usable,
|
||||
// an error occurred while trying to decode the media resource, resulting in an error.
|
||||
PlayerErrorDecode = 3
|
||||
|
||||
// PlayerErrorSourceNotSupported - MediaPlayer error code: The associated resource or media provider object has been found to be unsuitable.
|
||||
PlayerErrorSourceNotSupported = 4
|
||||
)
|
||||
|
||||
type MediaPlayer interface {
|
||||
View
|
||||
|
||||
// Play attempts to begin playback of the media.
|
||||
Play()
|
||||
|
||||
// Pause will pause playback of the media, if the media is already in a paused state this method will have no effect.
|
||||
Pause()
|
||||
|
||||
// SetCurrentTime sets the current playback time in seconds.
|
||||
SetCurrentTime(seconds float64)
|
||||
|
||||
// CurrentTime returns the current playback time in seconds.
|
||||
CurrentTime() float64
|
||||
|
||||
// Duration returns the value indicating the total duration of the media in seconds.
|
||||
// If no media data is available, the returned value is NaN.
|
||||
Duration() float64
|
||||
|
||||
// SetPlaybackRate sets the rate at which the media is being played back. This is used to implement user controls
|
||||
// for fast forward, slow motion, and so forth. The normal playback rate is multiplied by this value to obtain
|
||||
// the current rate, so a value of 1.0 indicates normal speed.
|
||||
SetPlaybackRate(rate float64)
|
||||
|
||||
// PlaybackRate returns the rate at which the media is being played back.
|
||||
PlaybackRate() float64
|
||||
|
||||
// SetVolume sets the audio volume, from 0.0 (silent) to 1.0 (loudest).
|
||||
SetVolume(volume float64)
|
||||
|
||||
// Volume returns the audio volume, from 0.0 (silent) to 1.0 (loudest).
|
||||
Volume() float64
|
||||
|
||||
// IsEnded function tells whether the media element is ended.
|
||||
IsEnded() bool
|
||||
|
||||
// IsPaused function tells whether the media element is paused.
|
||||
IsPaused() bool
|
||||
}
|
||||
|
|
|
@ -82,24 +82,32 @@ const (
|
|||
|
||||
// PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state
|
||||
PrimaryMouseButton = 0
|
||||
|
||||
// AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button
|
||||
// or the middle button (if present)
|
||||
AuxiliaryMouseButton = 1
|
||||
|
||||
// SecondaryMouseButton is a number of the secondary pressed button, usually the right button
|
||||
SecondaryMouseButton = 2
|
||||
|
||||
// MouseButton4 is a number of the fourth button, typically the Browser Back button
|
||||
MouseButton4 = 3
|
||||
|
||||
// MouseButton5 is a number of the fifth button, typically the Browser Forward button
|
||||
MouseButton5 = 4
|
||||
|
||||
// PrimaryMouseMask is the mask of the primary button (usually the left button)
|
||||
PrimaryMouseMask = 1
|
||||
|
||||
// SecondaryMouseMask is the mask of the secondary button (usually the right button)
|
||||
SecondaryMouseMask = 2
|
||||
|
||||
// AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button)
|
||||
AuxiliaryMouseMask = 4
|
||||
|
||||
// MouseMask4 is the mask of the 4th button (typically the "Browser Back" button)
|
||||
MouseMask4 = 8
|
||||
|
||||
//MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button)
|
||||
MouseMask5 = 16
|
||||
)
|
||||
|
|
|
@ -7,17 +7,37 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// NumberChangedEvent is the constant for the "" property tag.
|
||||
// The "number-changed" property sets listener(s) that track the change in the entered value.
|
||||
NumberChangedEvent = "number-changed"
|
||||
NumberPickerType = "number-picker-type"
|
||||
NumberPickerMin = "number-picker-min"
|
||||
NumberPickerMax = "number-picker-max"
|
||||
NumberPickerStep = "number-picker-step"
|
||||
NumberPickerValue = "number-picker-value"
|
||||
|
||||
// NumberPickerType is the constant for the "number-picker-type" property tag.
|
||||
// The "number-picker-type" int property sets the mode of NumberPicker. It can take the following values:
|
||||
// * NumberEditor (0) - NumberPicker is presented by editor. Default value;
|
||||
// * NumberSlider (1) - NumberPicker is presented by slider. |
|
||||
NumberPickerType = "number-picker-type"
|
||||
|
||||
// NumberPickerMin is the constant for the "number-picker-min" property tag.
|
||||
// The "number-picker-min" int property sets the minimum value of NumberPicker. The default value is 0.
|
||||
NumberPickerMin = "number-picker-min"
|
||||
|
||||
// NumberPickerMax is the constant for the "number-picker-max" property tag.
|
||||
// The "number-picker-max" int property sets the maximum value of NumberPicker. The default value is 1.
|
||||
NumberPickerMax = "number-picker-max"
|
||||
|
||||
// NumberPickerStep is the constant for the "number-picker-step" property tag.
|
||||
// The "number-picker-step" int property sets the value change step of NumberPicker
|
||||
NumberPickerStep = "number-picker-step"
|
||||
|
||||
// NumberPickerValue is the constant for the "number-picker-value" property tag.
|
||||
// The "number-picker-value" int property sets the current value of NumberPicker. The default value is 0.
|
||||
NumberPickerValue = "number-picker-value"
|
||||
)
|
||||
|
||||
const (
|
||||
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor
|
||||
NumberEditor = 0
|
||||
|
||||
// NumberSlider - type of NumberPicker. NumberPicker is presented by slider
|
||||
NumberSlider = 1
|
||||
)
|
||||
|
@ -47,6 +67,7 @@ func newNumberPicker(session Session) View {
|
|||
func (picker *numberPickerData) init(session Session) {
|
||||
picker.viewData.init(session)
|
||||
picker.tag = "NumberPicker"
|
||||
picker.hasHtmlDisabled = true
|
||||
picker.numberChangedListeners = []func(NumberPicker, float64, float64){}
|
||||
}
|
||||
|
||||
|
@ -232,13 +253,6 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde
|
|||
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
|
||||
}
|
||||
|
||||
func (picker *numberPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
if IsDisabled(self) {
|
||||
buffer.WriteString(` disabled`)
|
||||
}
|
||||
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||
}
|
||||
|
||||
func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||
switch command {
|
||||
case "textChanged":
|
||||
|
|
|
@ -11,15 +11,19 @@ type Properties interface {
|
|||
// The type of return value depends on the property. If the property is not set then nil is returned.
|
||||
Get(tag string) any
|
||||
getRaw(tag string) any
|
||||
|
||||
// Set sets the value (second argument) of the property with name defined by the first argument.
|
||||
// Return "true" if the value has been set, in the opposite case "false" are returned and
|
||||
// a description of the error is written to the log
|
||||
Set(tag string, value any) bool
|
||||
setRaw(tag string, value any)
|
||||
|
||||
// Remove removes the property with name defined by the argument
|
||||
Remove(tag string)
|
||||
|
||||
// Clear removes all properties
|
||||
Clear()
|
||||
|
||||
// AllTags returns an array of the set properties
|
||||
AllTags() []string
|
||||
}
|
||||
|
@ -68,6 +72,28 @@ func (properties *propertyList) AllTags() []string {
|
|||
return tags
|
||||
}
|
||||
|
||||
func (properties *propertyList) writeToBuffer(buffer *strings.Builder,
|
||||
indent string, objectTag string, tags []string) {
|
||||
|
||||
buffer.WriteString(objectTag)
|
||||
buffer.WriteString(" {\n")
|
||||
|
||||
indent2 := indent + "\t"
|
||||
|
||||
for _, tag := range tags {
|
||||
if value, ok := properties.properties[tag]; ok {
|
||||
buffer.WriteString(indent2)
|
||||
buffer.WriteString(tag)
|
||||
buffer.WriteString(" = ")
|
||||
writePropertyValue(buffer, tag, value, indent2)
|
||||
buffer.WriteString(",\n")
|
||||
}
|
||||
}
|
||||
|
||||
buffer.WriteString(indent)
|
||||
buffer.WriteString("}")
|
||||
}
|
||||
|
||||
func parseProperties(properties Properties, object DataObject) {
|
||||
count := object.PropertyCount()
|
||||
for i := 0; i < count; i++ {
|
||||
|
|
|
@ -666,7 +666,8 @@ const (
|
|||
|
||||
// Resize is the constant for the "resize" property tag.
|
||||
// The "resize" int property sets whether an element is resizable, and if so, in which directions.
|
||||
// Valid values are "none" (0), "both" (1), horizontal (2), and "vertical" (3)
|
||||
// Valid values are "none" / NoneResize (0), "both" / BothResize (1),
|
||||
// "horizontal" / HorizontalResize (2), and "vertical" / VerticalResize (3)
|
||||
Resize = "resize"
|
||||
|
||||
// UserSelect is the constant for the "user-select" property tag.
|
||||
|
|
|
@ -331,6 +331,16 @@ var enumProperties = map[string]struct {
|
|||
"justify-items",
|
||||
[]string{"start", "end", "center", "stretch"},
|
||||
},
|
||||
CellVerticalSelfAlign: {
|
||||
[]string{"top", "bottom", "center", "stretch"},
|
||||
"align-self",
|
||||
[]string{"start", "end", "center", "stretch"},
|
||||
},
|
||||
CellHorizontalSelfAlign: {
|
||||
[]string{"left", "right", "center", "stretch"},
|
||||
"justify-self",
|
||||
[]string{"start", "end", "center", "stretch"},
|
||||
},
|
||||
GridAutoFlow: {
|
||||
[]string{"row", "column", "row-dense", "column-dense"},
|
||||
GridAutoFlow,
|
||||
|
|
|
@ -11,22 +11,23 @@ const (
|
|||
// The "side" int property determines which side of the container is used to resize.
|
||||
// The value of property is or-combination of TopSide (1), RightSide (2), BottomSide (4), and LeftSide (8)
|
||||
Side = "side"
|
||||
|
||||
// ResizeBorderWidth is the constant for the "resize-border-width" property tag.
|
||||
// The "ResizeBorderWidth" SizeUnit property determines the width of the resizing border
|
||||
ResizeBorderWidth = "resize-border-width"
|
||||
// CellVerticalAlign is the constant for the "cell-vertical-align" property tag.
|
||||
CellVerticalAlign = "cell-vertical-align"
|
||||
// CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag.
|
||||
CellHorizontalAlign = "cell-horizontal-align"
|
||||
|
||||
// TopSide is value of the "side" property: the top side is used to resize
|
||||
TopSide = 1
|
||||
|
||||
// RightSide is value of the "side" property: the right side is used to resize
|
||||
RightSide = 2
|
||||
|
||||
// BottomSide is value of the "side" property: the bottom side is used to resize
|
||||
BottomSide = 4
|
||||
|
||||
// LeftSide is value of the "side" property: the left side is used to resize
|
||||
LeftSide = 8
|
||||
|
||||
// AllSides is value of the "side" property: all sides is used to resize
|
||||
AllSides = TopSide | RightSide | BottomSide | LeftSide
|
||||
)
|
||||
|
|
121
session.go
121
session.go
|
@ -7,7 +7,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type webBridge interface {
|
||||
type bridge interface {
|
||||
startUpdateScript(htmlID string) bool
|
||||
finishUpdateScript(htmlID string)
|
||||
callFunc(funcName string, args ...any) bool
|
||||
|
@ -16,10 +16,9 @@ type webBridge interface {
|
|||
updateCSSProperty(htmlID, property, value string)
|
||||
updateProperty(htmlID, property string, value any)
|
||||
removeProperty(htmlID, property string)
|
||||
readMessage() (string, bool)
|
||||
writeMessage(text string) bool
|
||||
addAnimationCSS(css string)
|
||||
clearAnimation()
|
||||
sendResponse()
|
||||
setAnimationCSS(css string)
|
||||
appendAnimationCSS(css string)
|
||||
canvasStart(htmlID string)
|
||||
callCanvasFunc(funcName string, args ...any)
|
||||
callCanvasVarFunc(v any, funcName string, args ...any)
|
||||
|
@ -111,6 +110,14 @@ type Session interface {
|
|||
// Invoke SetHotKey(..., ..., nil) for remove hotkey function.
|
||||
SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session))
|
||||
|
||||
// StartTimer starts a timer on the client side.
|
||||
// The first argument specifies the timer period in milliseconds.
|
||||
// The second argument specifies a function that will be called on each timer event.
|
||||
// The result is the id of the timer, which is used to stop the timer
|
||||
StartTimer(ms int, timerFunc func(Session)) int
|
||||
// StopTimer the timer with the given id
|
||||
StopTimer(timerID int)
|
||||
|
||||
getCurrentTheme() Theme
|
||||
registerAnimation(props []AnimatedProperty) string
|
||||
|
||||
|
@ -124,7 +131,7 @@ type Session interface {
|
|||
nextViewID() string
|
||||
styleProperty(styleTag, property string) any
|
||||
|
||||
setBridge(events chan DataObject, bridge webBridge)
|
||||
setBridge(events chan DataObject, bridge bridge)
|
||||
writeInitScript(writer *strings.Builder)
|
||||
callFunc(funcName string, args ...any)
|
||||
updateInnerHTML(htmlID, html string)
|
||||
|
@ -134,6 +141,7 @@ type Session interface {
|
|||
removeProperty(htmlID, property string)
|
||||
startUpdateScript(htmlID string) bool
|
||||
finishUpdateScript(htmlID string)
|
||||
sendResponse()
|
||||
addAnimationCSS(css string)
|
||||
clearAnimation()
|
||||
canvasStart(htmlID string)
|
||||
|
@ -145,7 +153,8 @@ type Session interface {
|
|||
canvasFinish()
|
||||
canvasTextMetrics(htmlID, font, text string) TextMetrics
|
||||
htmlPropertyValue(htmlID, name string) string
|
||||
handleAnswer(data DataObject)
|
||||
addToEventsQueue(data DataObject)
|
||||
handleAnswer(command string, data DataObject) bool
|
||||
handleRootSize(data DataObject)
|
||||
handleResize(data DataObject)
|
||||
handleEvent(command string, data DataObject)
|
||||
|
@ -189,13 +198,16 @@ type sessionData struct {
|
|||
ignoreUpdates bool
|
||||
popups *popupManager
|
||||
images *imageManager
|
||||
bridge webBridge
|
||||
bridge bridge
|
||||
events chan DataObject
|
||||
animationCounter int
|
||||
animationCSS string
|
||||
updateScripts map[string]*strings.Builder
|
||||
clientStorage map[string]string
|
||||
hotkeys map[string]func(Session)
|
||||
timers map[int]func(Session)
|
||||
nextTimerID int
|
||||
pauseTime int64
|
||||
}
|
||||
|
||||
func newSession(app Application, id int, customTheme string, params DataObject) Session {
|
||||
|
@ -214,6 +226,8 @@ func newSession(app Application, id int, customTheme string, params DataObject)
|
|||
session.updateScripts = map[string]*strings.Builder{}
|
||||
session.clientStorage = map[string]string{}
|
||||
session.hotkeys = map[string]func(Session){}
|
||||
session.timers = map[int]func(Session){}
|
||||
session.nextTimerID = 1
|
||||
|
||||
if customTheme != "" {
|
||||
if theme, ok := CreateThemeFromText(customTheme); ok {
|
||||
|
@ -237,7 +251,7 @@ func (session *sessionData) ID() int {
|
|||
return session.sessionID
|
||||
}
|
||||
|
||||
func (session *sessionData) setBridge(events chan DataObject, bridge webBridge) {
|
||||
func (session *sessionData) setBridge(events chan DataObject, bridge bridge) {
|
||||
session.events = events
|
||||
session.bridge = bridge
|
||||
}
|
||||
|
@ -330,23 +344,19 @@ func (session *sessionData) updateTooltipConstants() {
|
|||
}
|
||||
|
||||
func (session *sessionData) reload() {
|
||||
buffer := allocStringBuilder()
|
||||
defer freeStringBuilder(buffer)
|
||||
|
||||
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
|
||||
css = strings.ReplaceAll(css, "\n", `\n`)
|
||||
css = strings.ReplaceAll(css, "\t", `\t`)
|
||||
buffer.WriteString(`document.querySelector('style').textContent = "`)
|
||||
buffer.WriteString(css)
|
||||
buffer.WriteString("\";\n")
|
||||
session.bridge.callFunc("setStyles", css)
|
||||
|
||||
if session.rootView != nil {
|
||||
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
|
||||
buffer := allocStringBuilder()
|
||||
defer freeStringBuilder(buffer)
|
||||
|
||||
viewHTML(session.rootView, buffer)
|
||||
buffer.WriteString("';\nscanElementsSize();")
|
||||
session.bridge.updateInnerHTML("ruiRootView", buffer.String())
|
||||
session.bridge.callFunc("scanElementsSize")
|
||||
}
|
||||
|
||||
session.bridge.writeMessage(buffer.String())
|
||||
session.updateTooltipConstants()
|
||||
}
|
||||
|
||||
|
@ -447,15 +457,21 @@ func (session *sessionData) finishUpdateScript(htmlID string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (session *sessionData) sendResponse() {
|
||||
if session.bridge != nil {
|
||||
session.bridge.sendResponse()
|
||||
}
|
||||
}
|
||||
|
||||
func (session *sessionData) addAnimationCSS(css string) {
|
||||
if session.bridge != nil {
|
||||
session.bridge.addAnimationCSS(css)
|
||||
session.bridge.appendAnimationCSS(css)
|
||||
}
|
||||
}
|
||||
|
||||
func (session *sessionData) clearAnimation() {
|
||||
if session.bridge != nil {
|
||||
session.bridge.clearAnimation()
|
||||
session.bridge.setAnimationCSS("")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -520,8 +536,27 @@ func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (session *sessionData) handleAnswer(data DataObject) {
|
||||
session.bridge.answerReceived(data)
|
||||
func (session *sessionData) handleAnswer(command string, data DataObject) bool {
|
||||
switch command {
|
||||
case "answer":
|
||||
if session.bridge != nil {
|
||||
session.bridge.answerReceived(data)
|
||||
}
|
||||
|
||||
case "imageLoaded":
|
||||
session.imageManager().imageLoaded(data)
|
||||
|
||||
case "imageError":
|
||||
session.imageManager().imageLoadError(data)
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
if session.bridge != nil {
|
||||
session.bridge.sendResponse()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (session *sessionData) handleRootSize(data DataObject) {
|
||||
|
@ -642,6 +677,22 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
|
|||
case "session-resume":
|
||||
session.onResume()
|
||||
|
||||
case "timer":
|
||||
if text, ok := data.PropertyValue("timerID"); ok {
|
||||
timerID, err := strconv.Atoi(text)
|
||||
if err == nil {
|
||||
if fn, ok := session.timers[timerID]; ok {
|
||||
fn(session)
|
||||
} else {
|
||||
ErrorLog(`Timer (id = ` + text + `) not exists`)
|
||||
}
|
||||
} else {
|
||||
ErrorLog(err.Error())
|
||||
}
|
||||
} else {
|
||||
ErrorLog(`"timerID" property not found`)
|
||||
}
|
||||
|
||||
case "root-size":
|
||||
session.handleRootSize(data)
|
||||
|
||||
|
@ -672,6 +723,8 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
|
|||
ErrorLog(`"id" property not found. Event: ` + command)
|
||||
}
|
||||
}
|
||||
|
||||
session.bridge.sendResponse()
|
||||
}
|
||||
|
||||
func (session *sessionData) hotKey(event KeyEvent) {
|
||||
|
@ -769,3 +822,25 @@ func (session *sessionData) RemoveAllClientItems() {
|
|||
session.clientStorage = map[string]string{}
|
||||
session.bridge.callFunc("localStorageClear")
|
||||
}
|
||||
|
||||
func (session *sessionData) addToEventsQueue(data DataObject) {
|
||||
session.events <- data
|
||||
}
|
||||
|
||||
func (session *sessionData) StartTimer(ms int, timerFunc func(Session)) int {
|
||||
timerID := 0
|
||||
if session.bridge != nil {
|
||||
timerID = session.nextTimerID
|
||||
session.nextTimerID++
|
||||
session.timers[timerID] = timerFunc
|
||||
session.bridge.callFunc("startTimer", ms, timerID)
|
||||
}
|
||||
return timerID
|
||||
}
|
||||
|
||||
func (session *sessionData) StopTimer(timerID int) {
|
||||
if session.bridge != nil {
|
||||
session.bridge.callFunc("stopTimer", timerID)
|
||||
delete(session.timers, timerID)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package rui
|
||||
|
||||
import "time"
|
||||
|
||||
// SessionStartListener is the listener interface of a session start event
|
||||
type SessionStartListener interface {
|
||||
OnStart(session Session)
|
||||
|
@ -50,13 +52,25 @@ func (session *sessionData) onFinish() {
|
|||
|
||||
func (session *sessionData) onPause() {
|
||||
if session.content != nil {
|
||||
session.pauseTime = time.Now().Unix()
|
||||
if listener, ok := session.content.(SessionPauseListener); ok {
|
||||
listener.OnPause(session)
|
||||
}
|
||||
if timeout := session.app.Params().SocketAutoClose; timeout > 0 {
|
||||
go session.autoClose(session.pauseTime, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (session *sessionData) autoClose(start int64, timeout int) {
|
||||
time.Sleep(time.Second * time.Duration(timeout))
|
||||
if session.pauseTime == start {
|
||||
session.bridge.callFunc("closeSocket")
|
||||
}
|
||||
}
|
||||
|
||||
func (session *sessionData) onResume() {
|
||||
session.pauseTime = 0
|
||||
if session.content != nil {
|
||||
if listener, ok := session.content.(SessionResumeListener); ok {
|
||||
listener.OnResume(session)
|
||||
|
|
|
@ -22,10 +22,33 @@ const (
|
|||
// StackLayout - list-container of View
|
||||
type StackLayout interface {
|
||||
ViewsContainer
|
||||
|
||||
// Peek returns the current (visible) View. If StackLayout is empty then it returns nil.
|
||||
Peek() View
|
||||
|
||||
// RemovePeek removes the current View and returns it. If StackLayout is empty then it doesn't do anything and returns nil.
|
||||
RemovePeek() View
|
||||
|
||||
// MoveToFront makes the given View current. Returns true if successful, false otherwise.
|
||||
MoveToFront(view View) bool
|
||||
|
||||
// MoveToFrontByID makes the View current by viewID. Returns true if successful, false otherwise.
|
||||
MoveToFrontByID(viewID string) bool
|
||||
|
||||
// Push adds a new View to the container and makes it current.
|
||||
// It is similar to Append, but the addition is done using an animation effect.
|
||||
// The animation type is specified by the second argument and can take the following values:
|
||||
// * DefaultAnimation (0) - Default animation. For the Push function it is EndToStartAnimation, for Pop - StartToEndAnimation;
|
||||
// * StartToEndAnimation (1) - Animation from beginning to end. The beginning and the end are determined by the direction of the text output;
|
||||
// * EndToStartAnimation (2) - End-to-Beginning animation;
|
||||
// * TopDownAnimation (3) - Top-down animation;
|
||||
// * BottomUpAnimation (4) - Bottom up animation.
|
||||
// The third argument `onPushFinished` is the function to be called when the animation ends. It may be nil.
|
||||
Push(view View, animation int, onPushFinished func())
|
||||
|
||||
// Pop removes the current View from the container using animation.
|
||||
// The second argument `onPopFinished`` is the function to be called when the animation ends. It may be nil.
|
||||
// The function will return false if the StackLayout is empty and true if the current item has been removed.
|
||||
Pop(animation int, onPopFinished func(View)) bool
|
||||
}
|
||||
|
||||
|
@ -277,6 +300,10 @@ func (layout *stackLayoutData) RemoveView(index int) View {
|
|||
return layout.viewsContainerData.RemoveView(index)
|
||||
}
|
||||
|
||||
func (layout *stackLayoutData) RemovePeek() View {
|
||||
return layout.RemoveView(len(layout.views) - 1)
|
||||
}
|
||||
|
||||
func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) {
|
||||
if view == nil {
|
||||
ErrorLog("StackLayout.Push(nil, ....) is forbidden")
|
||||
|
|
|
@ -40,6 +40,7 @@ func newTimePicker(session Session) View {
|
|||
func (picker *timePickerData) init(session Session) {
|
||||
picker.viewData.init(session)
|
||||
picker.tag = "TimePicker"
|
||||
picker.hasHtmlDisabled = true
|
||||
picker.timeChangedListeners = []func(TimePicker, time.Time, time.Time){}
|
||||
}
|
||||
|
||||
|
@ -291,13 +292,6 @@ func (picker *timePickerData) htmlProperties(self View, buffer *strings.Builder)
|
|||
}
|
||||
}
|
||||
|
||||
func (picker *timePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
if IsDisabled(self) {
|
||||
buffer.WriteString(` disabled`)
|
||||
}
|
||||
picker.viewData.htmlDisabledProperties(self, buffer)
|
||||
}
|
||||
|
||||
func (picker *timePickerData) handleCommand(self View, command string, data DataObject) bool {
|
||||
switch command {
|
||||
case "textChanged":
|
||||
|
|
|
@ -8,9 +8,11 @@ const (
|
|||
// VideoWidth is the constant for the "video-width" property tag of VideoPlayer.
|
||||
// The "video-width" float property defines the width of the video's display area in pixels.
|
||||
VideoWidth = "video-width"
|
||||
|
||||
// VideoHeight is the constant for the "video-height" property tag of VideoPlayer.
|
||||
// The "video-height" float property defines the height of the video's display area in pixels.
|
||||
VideoHeight = "video-height"
|
||||
|
||||
// Poster is the constant for the "poster" property tag of VideoPlayer.
|
||||
// The "poster" property defines an URL for an image to be shown while the video is downloading.
|
||||
// If this attribute isn't specified, nothing is displayed until the first frame is available,
|
||||
|
|
87
view.go
87
view.go
|
@ -35,24 +35,33 @@ type View interface {
|
|||
|
||||
// Session returns the current Session interface
|
||||
Session() Session
|
||||
|
||||
// Parent returns the parent view
|
||||
Parent() View
|
||||
|
||||
// Tag returns the tag of View interface
|
||||
Tag() string
|
||||
|
||||
// ID returns the id of the view
|
||||
ID() string
|
||||
|
||||
// Focusable returns true if the view receives the focus
|
||||
Focusable() bool
|
||||
|
||||
// Frame returns the location and size of the view in pixels
|
||||
Frame() Frame
|
||||
|
||||
// Scroll returns the location size of the scrollable view in pixels
|
||||
Scroll() Frame
|
||||
|
||||
// SetAnimated sets the value (second argument) of the property with name defined by the first argument.
|
||||
// Return "true" if the value has been set, in the opposite case "false" are returned and
|
||||
// a description of the error is written to the log
|
||||
SetAnimated(tag string, value any, animation Animation) bool
|
||||
|
||||
// SetChangeListener set the function to track the change of the View property
|
||||
SetChangeListener(tag string, listener func(View, string))
|
||||
|
||||
// HasFocus returns 'true' if the view has focus
|
||||
HasFocus() bool
|
||||
|
||||
|
@ -65,7 +74,6 @@ type View interface {
|
|||
setParentID(parentID string)
|
||||
htmlSubviews(self View, buffer *strings.Builder)
|
||||
htmlProperties(self View, buffer *strings.Builder)
|
||||
htmlDisabledProperties(self View, buffer *strings.Builder)
|
||||
cssStyle(self View, builder cssBuilder)
|
||||
addToCSSStyle(addCSS map[string]string)
|
||||
|
||||
|
@ -93,6 +101,7 @@ type viewData struct {
|
|||
noResizeEvent bool
|
||||
created bool
|
||||
hasFocus bool
|
||||
hasHtmlDisabled bool
|
||||
//animation map[string]AnimationEndListener
|
||||
}
|
||||
|
||||
|
@ -135,6 +144,7 @@ func (view *viewData) init(session Session) {
|
|||
view.singleTransition = map[string]Animation{}
|
||||
view.noResizeEvent = false
|
||||
view.created = false
|
||||
view.hasHtmlDisabled = false
|
||||
}
|
||||
|
||||
func (view *viewData) Session() Session {
|
||||
|
@ -302,7 +312,6 @@ func (view *viewData) propertyChangedEvent(tag string) {
|
|||
if listener, ok := view.changeListener[tag]; ok {
|
||||
listener(view, tag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (view *viewData) Set(tag string, value any) bool {
|
||||
|
@ -404,7 +413,35 @@ func viewPropertyChanged(view *viewData, tag string) {
|
|||
|
||||
switch tag {
|
||||
case Disabled:
|
||||
updateInnerHTML(view.parentHTMLID(), session)
|
||||
tabIndex := GetTabIndex(view, htmlID)
|
||||
enabledClass := view.htmlClass(false)
|
||||
disabledClass := view.htmlClass(true)
|
||||
session.startUpdateScript(htmlID)
|
||||
if IsDisabled(view) {
|
||||
session.updateProperty(htmlID, "data-disabled", "1")
|
||||
if view.hasHtmlDisabled {
|
||||
session.updateProperty(htmlID, "disabled", true)
|
||||
}
|
||||
if tabIndex >= 0 {
|
||||
session.updateProperty(htmlID, "tabindex", -1)
|
||||
}
|
||||
if enabledClass != disabledClass {
|
||||
session.updateProperty(htmlID, "class", disabledClass)
|
||||
}
|
||||
} else {
|
||||
session.updateProperty(htmlID, "data-disabled", "0")
|
||||
if view.hasHtmlDisabled {
|
||||
session.removeProperty(htmlID, "disabled")
|
||||
}
|
||||
if tabIndex >= 0 {
|
||||
session.updateProperty(htmlID, "tabindex", tabIndex)
|
||||
}
|
||||
if enabledClass != disabledClass {
|
||||
session.updateProperty(htmlID, "class", enabledClass)
|
||||
}
|
||||
}
|
||||
session.finishUpdateScript(htmlID)
|
||||
updateInnerHTML(htmlID, session)
|
||||
return
|
||||
|
||||
case Visibility:
|
||||
|
@ -613,6 +650,8 @@ func viewPropertyChanged(view *viewData, tag string) {
|
|||
case ZIndex, Order, TabSize:
|
||||
if i, ok := intProperty(view, tag, session, 0); ok {
|
||||
session.updateCSSProperty(htmlID, tag, strconv.Itoa(i))
|
||||
} else {
|
||||
session.updateCSSProperty(htmlID, tag, "")
|
||||
}
|
||||
return
|
||||
|
||||
|
@ -660,8 +699,11 @@ func viewPropertyChanged(view *viewData, tag string) {
|
|||
}
|
||||
|
||||
if cssTag, ok := sizeProperties[tag]; ok {
|
||||
size, _ := sizeProperty(view, tag, session)
|
||||
session.updateCSSProperty(htmlID, cssTag, size.cssString("", session))
|
||||
if size, ok := sizeProperty(view, tag, session); ok {
|
||||
session.updateCSSProperty(htmlID, cssTag, size.cssString("", session))
|
||||
} else {
|
||||
session.updateCSSProperty(htmlID, cssTag, "")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -682,8 +724,11 @@ func viewPropertyChanged(view *viewData, tag string) {
|
|||
}
|
||||
|
||||
if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" {
|
||||
n, _ := enumProperty(view, tag, session, 0)
|
||||
session.updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n])
|
||||
if n, ok := enumProperty(view, tag, session, 0); ok {
|
||||
session.updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n])
|
||||
} else {
|
||||
session.updateCSSProperty(htmlID, valuesData.cssTag, "")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -691,6 +736,8 @@ func viewPropertyChanged(view *viewData, tag string) {
|
|||
if tag == floatTag {
|
||||
if f, ok := floatTextProperty(view, floatTag, session, 0); ok {
|
||||
session.updateCSSProperty(htmlID, floatTag, f)
|
||||
} else {
|
||||
session.updateCSSProperty(htmlID, floatTag, "")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -759,20 +806,22 @@ func (view *viewData) cssStyle(self View, builder cssBuilder) {
|
|||
|
||||
func (view *viewData) htmlProperties(self View, buffer *strings.Builder) {
|
||||
view.created = true
|
||||
|
||||
if IsDisabled(self) {
|
||||
buffer.WriteString(` data-disabled="1"`)
|
||||
if view.hasHtmlDisabled {
|
||||
buffer.WriteString(` disabled`)
|
||||
}
|
||||
} else {
|
||||
buffer.WriteString(` data-disabled="0"`)
|
||||
}
|
||||
|
||||
if view.frame.Left != 0 || view.frame.Top != 0 || view.frame.Width != 0 || view.frame.Height != 0 {
|
||||
buffer.WriteString(fmt.Sprintf(` data-left="%g" data-top="%g" data-width="%g" data-height="%g"`,
|
||||
view.frame.Left, view.frame.Top, view.frame.Width, view.frame.Height))
|
||||
}
|
||||
}
|
||||
|
||||
func (view *viewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
|
||||
if IsDisabled(self) {
|
||||
buffer.WriteString(` data-disabled="1"`)
|
||||
} else {
|
||||
buffer.WriteString(` data-disabled="0"`)
|
||||
}
|
||||
}
|
||||
|
||||
func viewHTML(view View, buffer *strings.Builder) {
|
||||
viewHTMLTag := view.htmlTag()
|
||||
buffer.WriteRune('<')
|
||||
|
@ -800,8 +849,6 @@ func viewHTML(view View, buffer *strings.Builder) {
|
|||
|
||||
buffer.WriteRune(' ')
|
||||
view.htmlProperties(view, buffer)
|
||||
buffer.WriteRune(' ')
|
||||
view.htmlDisabledProperties(view, buffer)
|
||||
|
||||
if view.isNoResizeEvent() {
|
||||
buffer.WriteString(` data-noresize="1" `)
|
||||
|
@ -810,12 +857,10 @@ func viewHTML(view View, buffer *strings.Builder) {
|
|||
}
|
||||
|
||||
if !disabled {
|
||||
if value, ok := intProperty(view, TabIndex, view.Session(), -1); ok {
|
||||
if tabIndex := GetTabIndex(view); tabIndex >= 0 {
|
||||
buffer.WriteString(`tabindex="`)
|
||||
buffer.WriteString(strconv.Itoa(value))
|
||||
buffer.WriteString(strconv.Itoa(tabIndex))
|
||||
buffer.WriteString(`" `)
|
||||
} else if view.Focusable() {
|
||||
buffer.WriteString(`tabindex="0" `)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
49
viewStyle.go
49
viewStyle.go
|
@ -13,8 +13,10 @@ type ViewStyle interface {
|
|||
|
||||
// Transition returns the transition animation of the property. Returns nil is there is no transition animation.
|
||||
Transition(tag string) Animation
|
||||
|
||||
// Transitions returns the map of transition animations. The result is always non-nil.
|
||||
Transitions() map[string]Animation
|
||||
|
||||
// SetTransition sets the transition animation for the property if "animation" argument is not nil, and
|
||||
// removes the transition animation of the property if "animation" argument is nil.
|
||||
// The "tag" argument is the property name.
|
||||
|
@ -573,6 +575,9 @@ func supportedPropertyValue(value any) bool {
|
|||
case []ViewShadow:
|
||||
case []View:
|
||||
case []any:
|
||||
case []BackgroundElement:
|
||||
case []BackgroundGradientPoint:
|
||||
case []BackgroundGradientAngle:
|
||||
case map[string]Animation:
|
||||
default:
|
||||
return false
|
||||
|
@ -692,6 +697,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
|
|||
for _, shadow := range value {
|
||||
buffer.WriteString(indent2)
|
||||
shadow.writeString(buffer, indent)
|
||||
buffer.WriteRune(',')
|
||||
}
|
||||
buffer.WriteRune('\n')
|
||||
buffer.WriteString(indent)
|
||||
|
@ -701,7 +707,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
|
|||
case []View:
|
||||
switch len(value) {
|
||||
case 0:
|
||||
buffer.WriteString("[]\n")
|
||||
buffer.WriteString("[]")
|
||||
|
||||
case 1:
|
||||
writeViewStyle(value[0].Tag(), value[0], buffer, indent)
|
||||
|
@ -740,6 +746,47 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
|
|||
buffer.WriteString(" ]")
|
||||
}
|
||||
|
||||
case []BackgroundElement:
|
||||
switch len(value) {
|
||||
case 0:
|
||||
buffer.WriteString("[]\n")
|
||||
|
||||
case 1:
|
||||
value[0].writeString(buffer, indent)
|
||||
|
||||
default:
|
||||
buffer.WriteString("[\n")
|
||||
indent2 := indent + "\t"
|
||||
for _, element := range value {
|
||||
buffer.WriteString(indent2)
|
||||
element.writeString(buffer, indent2)
|
||||
buffer.WriteString(",\n")
|
||||
}
|
||||
|
||||
buffer.WriteString(indent)
|
||||
buffer.WriteRune(']')
|
||||
}
|
||||
|
||||
case []BackgroundGradientPoint:
|
||||
buffer.WriteRune('"')
|
||||
for i, point := range value {
|
||||
if i > 0 {
|
||||
buffer.WriteString(",")
|
||||
}
|
||||
buffer.WriteString(point.String())
|
||||
}
|
||||
buffer.WriteRune('"')
|
||||
|
||||
case []BackgroundGradientAngle:
|
||||
buffer.WriteRune('"')
|
||||
for i, point := range value {
|
||||
if i > 0 {
|
||||
buffer.WriteString(",")
|
||||
}
|
||||
buffer.WriteString(point.String())
|
||||
}
|
||||
buffer.WriteRune('"')
|
||||
|
||||
case map[string]Animation:
|
||||
switch count := len(value); count {
|
||||
case 0:
|
||||
|
|
|
@ -32,42 +32,52 @@ func (style *viewStyle) setRange(tag string, value any) bool {
|
|||
}
|
||||
|
||||
func (style *viewStyle) setBackground(value any) bool {
|
||||
background := []BackgroundElement{}
|
||||
|
||||
switch value := value.(type) {
|
||||
case BackgroundElement:
|
||||
style.properties[Background] = []BackgroundElement{value}
|
||||
return true
|
||||
background = []BackgroundElement{value}
|
||||
|
||||
case []BackgroundElement:
|
||||
style.properties[Background] = value
|
||||
return true
|
||||
background = value
|
||||
|
||||
case []DataValue:
|
||||
for _, el := range value {
|
||||
if el.IsObject() {
|
||||
if element := createBackground(el.Object()); element != nil {
|
||||
background = append(background, element)
|
||||
}
|
||||
} else if obj := ParseDataText(el.Value()); obj != nil {
|
||||
if element := createBackground(obj); element != nil {
|
||||
background = append(background, element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case DataObject:
|
||||
if element := createBackground(value); element != nil {
|
||||
style.properties[Background] = []BackgroundElement{element}
|
||||
return true
|
||||
background = []BackgroundElement{element}
|
||||
}
|
||||
|
||||
case []DataObject:
|
||||
for _, obj := range value {
|
||||
background := []BackgroundElement{}
|
||||
if element := createBackground(obj); element != nil {
|
||||
background = append(background, element)
|
||||
}
|
||||
if len(background) > 0 {
|
||||
style.properties[Background] = background
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
case string:
|
||||
if obj := ParseDataText(value); obj != nil {
|
||||
if element := createBackground(obj); element != nil {
|
||||
style.properties[Background] = []BackgroundElement{element}
|
||||
return true
|
||||
background = []BackgroundElement{element}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(background) > 0 {
|
||||
style.properties[Background] = background
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -11,12 +11,16 @@ type ParentView interface {
|
|||
type ViewsContainer interface {
|
||||
View
|
||||
ParentView
|
||||
|
||||
// Append appends a view to the end of the list of a view children
|
||||
Append(view View)
|
||||
|
||||
// Insert inserts a view to the "index" position in the list of a view children
|
||||
Insert(view View, index int)
|
||||
|
||||
// Remove removes a view from the list of a view children and return it
|
||||
RemoveView(index int) View
|
||||
|
||||
// ViewIndex returns the index of view, -1 overwise
|
||||
ViewIndex(view View) int
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ type wasmBridge struct {
|
|||
closeEvent chan DataObject
|
||||
}
|
||||
|
||||
func createWasmBridge(close chan DataObject) webBridge {
|
||||
func createWasmBridge(close chan DataObject) bridge {
|
||||
bridge := new(wasmBridge)
|
||||
bridge.answerID = 1
|
||||
bridge.answer = make(map[int]chan DataObject)
|
||||
|
@ -102,10 +102,6 @@ func (bridge *wasmBridge) close() {
|
|||
bridge.closeEvent <- NewDataObject("close")
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) readMessage() (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) writeMessage(script string) bool {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run script:")
|
||||
|
@ -118,21 +114,24 @@ func (bridge *wasmBridge) writeMessage(script string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) addAnimationCSS(css string) {
|
||||
func (bridge *wasmBridge) prepareCSS(css string) string {
|
||||
css = strings.ReplaceAll(css, `\t`, "\t")
|
||||
css = strings.ReplaceAll(css, `\n`, "\n")
|
||||
css = strings.ReplaceAll(css, `\'`, "'")
|
||||
css = strings.ReplaceAll(css, `\"`, "\"")
|
||||
css = strings.ReplaceAll(css, `\\`, "\\")
|
||||
|
||||
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
|
||||
content := styles.Get("textContent").String()
|
||||
styles.Set("textContent", content+"\n"+css)
|
||||
return css
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) clearAnimation() {
|
||||
func (bridge *wasmBridge) appendAnimationCSS(css string) {
|
||||
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
|
||||
styles.Set("textContent", "")
|
||||
content := styles.Get("textContent").String()
|
||||
styles.Set("textContent", content+"\n"+bridge.prepareCSS(css))
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) setAnimationCSS(css string) {
|
||||
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
|
||||
styles.Set("textContent", bridge.prepareCSS(css))
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) canvasStart(htmlID string) {
|
||||
|
@ -276,3 +275,6 @@ func (bridge *wasmBridge) answerReceived(answer DataObject) {
|
|||
func (bridge *wasmBridge) remoteAddr() string {
|
||||
return "localhost"
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) sendResponse() {
|
||||
}
|
||||
|
|
314
webBridge.go
314
webBridge.go
|
@ -12,16 +12,30 @@ import (
|
|||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type webBridge struct {
|
||||
answer map[int]chan DataObject
|
||||
answerID int
|
||||
answerMutex sync.Mutex
|
||||
writeMutex sync.Mutex
|
||||
closed bool
|
||||
canvasBuffer strings.Builder
|
||||
canvasVarNumber int
|
||||
updateScripts map[string]*strings.Builder
|
||||
writeMessage func(string) bool
|
||||
callFuncImmediately func(funcName string, args ...any) bool
|
||||
}
|
||||
|
||||
type wsBridge struct {
|
||||
conn *websocket.Conn
|
||||
answer map[int]chan DataObject
|
||||
answerID int
|
||||
answerMutex sync.Mutex
|
||||
closed bool
|
||||
buffer strings.Builder
|
||||
canvasBuffer strings.Builder
|
||||
canvasVarNumber int
|
||||
updateScripts map[string]*strings.Builder
|
||||
webBridge
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
type httpBridge struct {
|
||||
webBridge
|
||||
responseBuffer strings.Builder
|
||||
response chan string
|
||||
remoteAddress string
|
||||
//conn *websocket.Conn
|
||||
}
|
||||
|
||||
type canvasVar struct {
|
||||
|
@ -33,7 +47,7 @@ var upgrader = websocket.Upgrader{
|
|||
WriteBufferSize: 8096,
|
||||
}
|
||||
|
||||
func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
|
||||
func createSocketBridge(w http.ResponseWriter, req *http.Request) *wsBridge {
|
||||
conn, err := upgrader.Upgrade(w, req, nil)
|
||||
if err != nil {
|
||||
ErrorLog(err.Error())
|
||||
|
@ -41,41 +55,84 @@ func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
|
|||
}
|
||||
|
||||
bridge := new(wsBridge)
|
||||
bridge.answerID = 1
|
||||
bridge.answer = make(map[int]chan DataObject)
|
||||
bridge.initBridge()
|
||||
bridge.conn = conn
|
||||
bridge.closed = false
|
||||
bridge.updateScripts = map[string]*strings.Builder{}
|
||||
bridge.writeMessage = func(script string) bool {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("🖥️ <- " + script)
|
||||
}
|
||||
|
||||
if bridge.conn == nil {
|
||||
ErrorLog("No connection")
|
||||
return false
|
||||
}
|
||||
|
||||
bridge.writeMutex.Lock()
|
||||
err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script))
|
||||
bridge.writeMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
ErrorLog(err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
bridge.callFuncImmediately = bridge.callFunc
|
||||
return bridge
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) close() {
|
||||
bridge.closed = true
|
||||
bridge.conn.Close()
|
||||
func createHttpBridge(req *http.Request) *httpBridge {
|
||||
bridge := new(httpBridge)
|
||||
bridge.initBridge()
|
||||
bridge.response = make(chan string, 10)
|
||||
bridge.writeMessage = func(script string) bool {
|
||||
if script != "" {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog(script)
|
||||
}
|
||||
|
||||
if bridge.responseBuffer.Len() > 0 {
|
||||
bridge.responseBuffer.WriteRune('\n')
|
||||
}
|
||||
bridge.responseBuffer.WriteString(script)
|
||||
}
|
||||
return true
|
||||
}
|
||||
bridge.callFuncImmediately = bridge.callImmediately
|
||||
bridge.remoteAddress = req.RemoteAddr
|
||||
return bridge
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) startUpdateScript(htmlID string) bool {
|
||||
func (bridge *webBridge) initBridge() {
|
||||
bridge.answerID = 1
|
||||
bridge.answer = make(map[int]chan DataObject)
|
||||
bridge.closed = false
|
||||
bridge.updateScripts = map[string]*strings.Builder{}
|
||||
}
|
||||
|
||||
func (bridge *webBridge) startUpdateScript(htmlID string) bool {
|
||||
if _, ok := bridge.updateScripts[htmlID]; ok {
|
||||
return false
|
||||
}
|
||||
buffer := allocStringBuilder()
|
||||
bridge.updateScripts[htmlID] = buffer
|
||||
buffer.WriteString("var element = document.getElementById('")
|
||||
buffer.WriteString("{\nlet element = document.getElementById('")
|
||||
buffer.WriteString(htmlID)
|
||||
buffer.WriteString("');\nif (element) {\n")
|
||||
return true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) finishUpdateScript(htmlID string) {
|
||||
func (bridge *webBridge) finishUpdateScript(htmlID string) {
|
||||
if buffer, ok := bridge.updateScripts[htmlID]; ok {
|
||||
buffer.WriteString("scanElementsSize();\n}\n")
|
||||
buffer.WriteString("scanElementsSize();\n}\n}\n")
|
||||
bridge.writeMessage(buffer.String())
|
||||
|
||||
freeStringBuilder(buffer)
|
||||
delete(bridge.updateScripts, htmlID)
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) argToString(arg any) (string, bool) {
|
||||
func (bridge *webBridge) argToString(arg any) (string, bool) {
|
||||
switch arg := arg.(type) {
|
||||
case string:
|
||||
arg = strings.ReplaceAll(arg, "\\", `\\`)
|
||||
|
@ -150,43 +207,44 @@ func (bridge *wsBridge) argToString(arg any) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) callFunc(funcName string, args ...any) bool {
|
||||
bridge.buffer.Reset()
|
||||
bridge.buffer.WriteString(funcName)
|
||||
bridge.buffer.WriteRune('(')
|
||||
func (bridge *webBridge) callFuncScript(funcName string, args ...any) (string, bool) {
|
||||
buffer := allocStringBuilder()
|
||||
defer freeStringBuilder(buffer)
|
||||
|
||||
buffer.WriteString(funcName)
|
||||
buffer.WriteRune('(')
|
||||
for i, arg := range args {
|
||||
argText, ok := bridge.argToString(arg)
|
||||
if !ok {
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
bridge.buffer.WriteString(", ")
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
bridge.buffer.WriteString(argText)
|
||||
buffer.WriteString(argText)
|
||||
}
|
||||
bridge.buffer.WriteString(");")
|
||||
buffer.WriteString(");")
|
||||
|
||||
funcText := bridge.buffer.String()
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run func: " + funcText)
|
||||
}
|
||||
if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(funcText)); err != nil {
|
||||
ErrorLog(err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return buffer.String(), true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) updateInnerHTML(htmlID, html string) {
|
||||
func (bridge *webBridge) callFunc(funcName string, args ...any) bool {
|
||||
if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
|
||||
return bridge.writeMessage(funcText)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (bridge *webBridge) updateInnerHTML(htmlID, html string) {
|
||||
bridge.callFunc("updateInnerHTML", htmlID, html)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) appendToInnerHTML(htmlID, html string) {
|
||||
func (bridge *webBridge) appendToInnerHTML(htmlID, html string) {
|
||||
bridge.callFunc("appendToInnerHTML", htmlID, html)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
|
||||
func (bridge *webBridge) updateCSSProperty(htmlID, property, value string) {
|
||||
if buffer, ok := bridge.updateScripts[htmlID]; ok {
|
||||
buffer.WriteString(`element.style['`)
|
||||
buffer.WriteString(property)
|
||||
|
@ -198,7 +256,7 @@ func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
|
||||
func (bridge *webBridge) updateProperty(htmlID, property string, value any) {
|
||||
if buffer, ok := bridge.updateScripts[htmlID]; ok {
|
||||
if val, ok := bridge.argToString(value); ok {
|
||||
buffer.WriteString(`element.setAttribute('`)
|
||||
|
@ -212,7 +270,7 @@ func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) removeProperty(htmlID, property string) {
|
||||
func (bridge *webBridge) removeProperty(htmlID, property string) {
|
||||
if buffer, ok := bridge.updateScripts[htmlID]; ok {
|
||||
buffer.WriteString(`if (element.hasAttribute('`)
|
||||
buffer.WriteString(property)
|
||||
|
@ -224,28 +282,34 @@ func (bridge *wsBridge) removeProperty(htmlID, property string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) addAnimationCSS(css string) {
|
||||
bridge.writeMessage(`var styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent += '` + css + `';
|
||||
func (bridge *webBridge) appendAnimationCSS(css string) {
|
||||
//bridge.callFunc("appendAnimationCSS", css)
|
||||
bridge.writeMessage(`{
|
||||
let styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent += '` + css + `';
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) clearAnimation() {
|
||||
bridge.writeMessage(`var styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent = '';
|
||||
func (bridge *webBridge) setAnimationCSS(css string) {
|
||||
//bridge.callFunc("setAnimationCSS", css)
|
||||
bridge.writeMessage(`{
|
||||
let styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent = '` + css + `';
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) canvasStart(htmlID string) {
|
||||
func (bridge *webBridge) canvasStart(htmlID string) {
|
||||
bridge.canvasBuffer.Reset()
|
||||
bridge.canvasBuffer.WriteString(`const ctx = getCanvasContext('`)
|
||||
bridge.canvasBuffer.WriteString("{\nconst ctx = getCanvasContext('")
|
||||
bridge.canvasBuffer.WriteString(htmlID)
|
||||
bridge.canvasBuffer.WriteString(`');`)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
|
||||
func (bridge *webBridge) callCanvasFunc(funcName string, args ...any) {
|
||||
bridge.canvasBuffer.WriteString("\nctx.")
|
||||
bridge.canvasBuffer.WriteString(funcName)
|
||||
bridge.canvasBuffer.WriteRune('(')
|
||||
|
@ -259,7 +323,7 @@ func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
|
|||
bridge.canvasBuffer.WriteString(");")
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
|
||||
func (bridge *webBridge) updateCanvasProperty(property string, value any) {
|
||||
bridge.canvasBuffer.WriteString("\nctx.")
|
||||
bridge.canvasBuffer.WriteString(property)
|
||||
bridge.canvasBuffer.WriteString(" = ")
|
||||
|
@ -268,10 +332,10 @@ func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
|
|||
bridge.canvasBuffer.WriteString(";")
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
|
||||
func (bridge *webBridge) createCanvasVar(funcName string, args ...any) any {
|
||||
bridge.canvasVarNumber++
|
||||
result := canvasVar{name: fmt.Sprintf("v%d", bridge.canvasVarNumber)}
|
||||
bridge.canvasBuffer.WriteString("\nvar ")
|
||||
bridge.canvasBuffer.WriteString("\nlet ")
|
||||
bridge.canvasBuffer.WriteString(result.name)
|
||||
bridge.canvasBuffer.WriteString(" = ctx.")
|
||||
bridge.canvasBuffer.WriteString(funcName)
|
||||
|
@ -287,7 +351,7 @@ func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
|
|||
return result
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
|
||||
func (bridge *webBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
|
||||
varName, ok := v.(canvasVar)
|
||||
if !ok {
|
||||
return
|
||||
|
@ -307,7 +371,7 @@ func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
|
|||
bridge.canvasBuffer.WriteString(");")
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
|
||||
func (bridge *webBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
|
||||
|
||||
bridge.canvasBuffer.WriteString("\nimg = images.get('")
|
||||
bridge.canvasBuffer.WriteString(url)
|
||||
|
@ -328,51 +392,12 @@ func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcNam
|
|||
bridge.canvasBuffer.WriteString(");\n}")
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) canvasFinish() {
|
||||
bridge.canvasBuffer.WriteString("\n")
|
||||
script := bridge.canvasBuffer.String()
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run script:")
|
||||
DebugLog(script)
|
||||
}
|
||||
if bridge.conn == nil {
|
||||
ErrorLog("No connection")
|
||||
} else if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
|
||||
ErrorLog(err.Error())
|
||||
}
|
||||
func (bridge *webBridge) canvasFinish() {
|
||||
bridge.canvasBuffer.WriteString("\n}\n")
|
||||
bridge.writeMessage(bridge.canvasBuffer.String())
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) readMessage() (string, bool) {
|
||||
_, p, err := bridge.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if !bridge.closed {
|
||||
ErrorLog(err.Error())
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(p), true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) writeMessage(script string) bool {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run script:")
|
||||
DebugLog(script)
|
||||
}
|
||||
if bridge.conn == nil {
|
||||
ErrorLog("No connection")
|
||||
return false
|
||||
}
|
||||
if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
|
||||
ErrorLog(err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
|
||||
result := TextMetrics{}
|
||||
|
||||
func (bridge *webBridge) remoteValue(funcName string, args ...any) (DataObject, bool) {
|
||||
bridge.answerMutex.Lock()
|
||||
answerID := bridge.answerID
|
||||
bridge.answerID++
|
||||
|
@ -381,36 +406,36 @@ func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics
|
|||
answer := make(chan DataObject)
|
||||
bridge.answer[answerID] = answer
|
||||
|
||||
if bridge.callFunc("canvasTextMetrics", answerID, htmlID, font, text) {
|
||||
data := <-answer
|
||||
result.Width = dataFloatProperty(data, "width")
|
||||
funcArgs := append([]any{answerID}, args...)
|
||||
var result DataObject = nil
|
||||
ok := bridge.callFuncImmediately(funcName, funcArgs...)
|
||||
if ok {
|
||||
result = <-answer
|
||||
}
|
||||
|
||||
close(answer)
|
||||
delete(bridge.answer, answerID)
|
||||
return result, true
|
||||
}
|
||||
|
||||
func (bridge *webBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
|
||||
result := TextMetrics{}
|
||||
if data, ok := bridge.remoteValue("canvasTextMetrics", htmlID, font, text); ok {
|
||||
result.Width = dataFloatProperty(data, "width")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) htmlPropertyValue(htmlID, name string) string {
|
||||
bridge.answerMutex.Lock()
|
||||
answerID := bridge.answerID
|
||||
bridge.answerID++
|
||||
bridge.answerMutex.Unlock()
|
||||
|
||||
answer := make(chan DataObject)
|
||||
bridge.answer[answerID] = answer
|
||||
|
||||
if bridge.callFunc("getPropertyValue", answerID, htmlID, name) {
|
||||
data := <-answer
|
||||
func (bridge *webBridge) htmlPropertyValue(htmlID, name string) string {
|
||||
if data, ok := bridge.remoteValue("getPropertyValue", htmlID, name); ok {
|
||||
if value, ok := data.PropertyValue("value"); ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
delete(bridge.answer, answerID)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) answerReceived(answer DataObject) {
|
||||
func (bridge *webBridge) answerReceived(answer DataObject) {
|
||||
if text, ok := answer.PropertyValue("answerID"); ok {
|
||||
if id, err := strconv.Atoi(text); err == nil {
|
||||
if chanel, ok := bridge.answer[id]; ok {
|
||||
|
@ -427,6 +452,55 @@ func (bridge *wsBridge) answerReceived(answer DataObject) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) close() {
|
||||
bridge.closed = true
|
||||
defer bridge.conn.Close()
|
||||
bridge.conn = nil
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) readMessage() (string, bool) {
|
||||
_, p, err := bridge.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if !bridge.closed {
|
||||
ErrorLog(err.Error())
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(p), true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) sendResponse() {
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) remoteAddr() string {
|
||||
return bridge.conn.RemoteAddr().String()
|
||||
}
|
||||
|
||||
func (bridge *httpBridge) close() {
|
||||
bridge.closed = true
|
||||
// TODO
|
||||
}
|
||||
|
||||
func (bridge *httpBridge) callImmediately(funcName string, args ...any) bool {
|
||||
if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run func: " + funcText)
|
||||
}
|
||||
bridge.response <- funcText
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (bridge *httpBridge) sendResponse() {
|
||||
bridge.writeMutex.Lock()
|
||||
text := bridge.responseBuffer.String()
|
||||
bridge.responseBuffer.Reset()
|
||||
bridge.writeMutex.Unlock()
|
||||
bridge.response <- text
|
||||
}
|
||||
|
||||
func (bridge *httpBridge) remoteAddr() string {
|
||||
return bridge.remoteAddress
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue