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