package rui import ( "fmt" "net/url" "strconv" "strings" ) type bridge interface { startUpdateScript(htmlID string) bool finishUpdateScript(htmlID string) callFunc(funcName string, args ...any) bool updateInnerHTML(htmlID, html string) appendToInnerHTML(htmlID, html string) updateCSSProperty(htmlID, property, value string) updateProperty(htmlID, property string, value any) removeProperty(htmlID, property string) sendResponse() setAnimationCSS(css string) appendAnimationCSS(css string) canvasStart(htmlID string) callCanvasFunc(funcName string, args ...any) callCanvasVarFunc(v any, funcName string, args ...any) callCanvasImageFunc(url string, property string, funcName string, args ...any) createCanvasVar(funcName string, args ...any) any createPath2D(arg string) any updateCanvasProperty(property string, value any) canvasFinish() canvasTextMetrics(htmlID, font, text string) TextMetrics htmlPropertyValue(htmlID, name string) string answerReceived(answer DataObject) close() remoteAddr() string } // SessionContent is the interface of a session content type SessionContent interface { CreateRootView(session Session) View } // Session provide interface to session parameters assess type Session interface { // App return the current application interface App() Application // ID return the id of the session ID() int // DarkTheme returns "true" if the dark theme is used DarkTheme() bool // Mobile returns "true" if current session is displayed on a touch screen device TouchScreen() bool // PixelRatio returns the ratio of the resolution in physical pixels to the resolution // in logical pixels for the current display device. PixelRatio() float64 // TextDirection returns the default text direction (LeftToRightDirection (1) or RightToLeftDirection (2)) TextDirection() int // Constant returns the constant with "tag" name or "" if it is not exists Constant(tag string) (string, bool) // Color returns the color with "tag" name or 0 if it is not exists Color(tag string) (Color, bool) // ImageConstant returns the image constant with "tag" name or "" if it is not exists ImageConstant(tag string) (string, bool) // SetCustomTheme set the custom theme SetCustomTheme(name string) bool // UserAgent returns the "user-agent" text of the client browser UserAgent() string // RemoteAddr returns the client address. RemoteAddr() string // Language returns the current session language Language() string // SetLanguage set the current session language SetLanguage(lang string) // GetString returns the text for the current language GetString(tag string) (string, bool) // Content returns the SessionContent of session Content() SessionContent setContent(content SessionContent) bool // SetTitle sets the text of the browser title/tab SetTitle(title string) // SetTitleColor sets the color of the browser navigation bar. Supported only in Safari and Chrome for android SetTitleColor(color Color) // RootView returns the root view of the session RootView() View // Get returns a value of the view (with id defined by the first argument) property with name defined by the second argument. // The type of return value depends on the property. If the property is not set then nil is returned. Get(viewID, tag string) any // Set sets the value (third argument) of the property (second argument) of the view with id defined by the first argument. // Return "true" if the value has been set, in the opposite case "false" are returned and // a description of the error is written to the log Set(viewID, tag string, value any) bool // DownloadFile downloads (saves) on the client side the file located at the specified path on the server. DownloadFile(path string) //DownloadFileData downloads (saves) on the client side a file with a specified name and specified content. DownloadFileData(filename string, data []byte) // OpenURL opens the url in the new browser tab OpenURL(url string) // ClientItem reads value by key from the client-side storage ClientItem(key string) (string, bool) // SetClientItem stores a key-value pair in the client-side storage SetClientItem(key, value string) // RemoveClientItem removes a key-value pair in the client-side storage RemoveClientItem(key string) // RemoveAllClientItems removes all key-value pair from the client-side storage RemoveAllClientItems() // SetHotKey sets the function that will be called when the given hotkey is pressed. // 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 resolveConstants(value string) (string, bool) checkboxOffImage(accentColor Color) string checkboxOnImage(accentColor Color) string radiobuttonOffImage() string radiobuttonOnImage(accentColor Color) string viewByHTMLID(id string) View nextViewID() string styleProperty(styleTag, property string) any setBridge(events chan DataObject, bridge bridge) writeInitScript(writer *strings.Builder) callFunc(funcName string, args ...any) updateInnerHTML(htmlID, html string) appendToInnerHTML(htmlID, html string) updateCSSProperty(htmlID, property, value string) updateProperty(htmlID, property string, value any) removeProperty(htmlID, property string) startUpdateScript(htmlID string) bool finishUpdateScript(htmlID string) sendResponse() addAnimationCSS(css string) removeAnimation(keyframe string) htmlPropertyValue(htmlID, name string) string canvasStart(htmlID string) bool callCanvasFunc(funcName string, args ...any) createCanvasVar(funcName string, args ...any) any createPath(arg string) any callCanvasVarFunc(v any, funcName string, args ...any) callCanvasImageFunc(url string, property string, funcName string, args ...any) updateCanvasProperty(property string, value any) canvasFinish() canvasTextMetrics(htmlID, font, text string) TextMetrics addToEventsQueue(data DataObject) handleAnswer(command string, data DataObject) bool handleRootSize(data DataObject) handleResize(data DataObject) handleEvent(command string, data DataObject) close() onStart() onFinish() onPause() onResume() onDisconnect() onReconnect() ignoreViewUpdates() bool setIgnoreViewUpdates(ignore bool) popupManager() *popupManager imageManager() *imageManager } type sessionData struct { customTheme Theme currentTheme Theme darkTheme bool touchScreen bool screenWidth int screenHeight int textDirection int pixelRatio float64 userAgent string language string languages []string checkboxOff string checkboxOn string radiobuttonOff string radiobuttonOn string app Application sessionID int viewCounter int content SessionContent rootView View ignoreUpdates bool popups *popupManager images *imageManager 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 { session := new(sessionData) session.app = app session.sessionID = id session.darkTheme = false session.touchScreen = false session.pixelRatio = 1 session.textDirection = LeftToRightDirection session.languages = []string{} session.viewCounter = 0 session.ignoreUpdates = false session.animationCounter = 0 session.animationCSS = "" 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 { session.customTheme = theme session.currentTheme = nil } } if params != nil { session.handleSessionInfo(params) } return session } func (session *sessionData) App() Application { return session.app } func (session *sessionData) ID() int { return session.sessionID } func (session *sessionData) setBridge(events chan DataObject, bridge bridge) { session.events = events session.bridge = bridge } func (session *sessionData) close() { if session.events != nil { session.events <- ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`) } } func (session *sessionData) styleProperty(styleTag, propertyTag string) any { if style := session.getCurrentTheme().style(styleTag); style != nil { return style.getRaw(propertyTag) } //errorLogF(`property "%v" not found`, propertyTag) return nil } func (session *sessionData) nextViewID() string { session.viewCounter++ return fmt.Sprintf("id%06d", session.viewCounter) } func (session *sessionData) viewByHTMLID(id string) View { if session.rootView == nil { return nil } popupManager := session.popupManager() for _, popup := range popupManager.popups { if view := popup.viewByHTMLID(id); view != nil { return view } } return viewByHTMLID(id, session.rootView) } func (session *sessionData) Content() SessionContent { return session.content } func (session *sessionData) setContent(content SessionContent) bool { if content != nil { session.content = content session.rootView = content.CreateRootView(session) if session.rootView != nil { session.rootView.setParentID("ruiRootView") return true } } return false } func (session *sessionData) RootView() View { return session.rootView } func (session *sessionData) writeInitScript(writer *strings.Builder) { if css := session.getCurrentTheme().cssText(session); css != "" { css = strings.ReplaceAll(css, "\n", `\n`) css = strings.ReplaceAll(css, "\t", `\t`) writer.WriteString(`document.querySelector('style').textContent += "`) writer.WriteString(css) writer.WriteString("\";\n") } if session.rootView != nil { writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) buffer := allocStringBuilder() defer freeStringBuilder(buffer) viewHTML(session.rootView, buffer) text := strings.ReplaceAll(buffer.String(), "'", `\'`) writer.WriteString(text) writer.WriteString("';\nscanElementsSize();") } session.updateTooltipConstants() } func (session *sessionData) updateTooltipConstants() { if color, ok := session.Color("ruiTooltipBackground"); ok { session.bridge.callFunc("setCssVar", "--tooltip-background", color.cssString()) } if color, ok := session.Color("ruiTooltipTextColor"); ok { session.bridge.callFunc("setCssVar", "--tooltip-text-color", color.cssString()) } if color, ok := session.Color("ruiTooltipShadowColor"); ok { session.bridge.callFunc("setCssVar", "--tooltip-shadow-color", color.cssString()) } } func (session *sessionData) reload() { css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS session.bridge.callFunc("setStyles", css) if session.rootView != nil { buffer := allocStringBuilder() defer freeStringBuilder(buffer) viewHTML(session.rootView, buffer) session.bridge.updateInnerHTML("ruiRootView", buffer.String()) session.bridge.callFunc("scanElementsSize") } session.updateTooltipConstants() } func (session *sessionData) ignoreViewUpdates() bool { return session.bridge == nil || session.ignoreUpdates } func (session *sessionData) setIgnoreViewUpdates(ignore bool) { session.ignoreUpdates = ignore } func (session *sessionData) Get(viewID, tag string) any { if view := ViewByID(session.RootView(), viewID); view != nil { return view.Get(tag) } return nil } func (session *sessionData) Set(viewID, tag string, value any) bool { if view := ViewByID(session.RootView(), viewID); view != nil { return view.Set(tag, value) } return false } func (session *sessionData) popupManager() *popupManager { if session.popups == nil { session.popups = new(popupManager) session.popups.popups = []Popup{} } return session.popups } func (session *sessionData) imageManager() *imageManager { if session.images == nil { session.images = new(imageManager) session.images.images = make(map[string]*imageData) } return session.images } func (session *sessionData) callFunc(funcName string, args ...any) { if session.bridge != nil { session.bridge.callFunc(funcName, args...) } else { ErrorLog("No connection") } } func (session *sessionData) updateInnerHTML(htmlID, html string) { if !session.ignoreViewUpdates() { if session.bridge != nil { session.bridge.updateInnerHTML(htmlID, html) } else { ErrorLog("No connection") } } } func (session *sessionData) appendToInnerHTML(htmlID, html string) { if !session.ignoreViewUpdates() { if session.bridge != nil { session.bridge.appendToInnerHTML(htmlID, html) } else { ErrorLog("No connection") } } } func (session *sessionData) updateCSSProperty(htmlID, property, value string) { if !session.ignoreViewUpdates() { if session.bridge != nil { session.bridge.updateCSSProperty(htmlID, property, value) } else { ErrorLog("No connection") } } } func (session *sessionData) updateProperty(htmlID, property string, value any) { if !session.ignoreViewUpdates() { if session.bridge != nil { session.bridge.updateProperty(htmlID, property, value) } else { ErrorLog("No connection") } } } func (session *sessionData) removeProperty(htmlID, property string) { if !session.ignoreViewUpdates() { if session.bridge != nil { session.bridge.removeProperty(htmlID, property) } else { ErrorLog("No connection") } } } func (session *sessionData) startUpdateScript(htmlID string) bool { if session.bridge != nil { return session.bridge.startUpdateScript(htmlID) } ErrorLog("No connection") return false } func (session *sessionData) finishUpdateScript(htmlID string) { if session.bridge != nil { session.bridge.finishUpdateScript(htmlID) } else { ErrorLog("No connection") } } func (session *sessionData) sendResponse() { if session.bridge != nil { session.bridge.sendResponse() } else { ErrorLog("No connection") } } func (session *sessionData) addAnimationCSS(css string) { session.animationCSS += css if session.bridge != nil { session.bridge.appendAnimationCSS(css) } else { ErrorLog("No connection") } } func (session *sessionData) removeAnimation(keyframe string) { css := session.animationCSS index := strings.Index(css, "@keyframes "+keyframe) if index < 0 { return } start := strings.IndexRune(css[index:], '{') if start < 0 { return } n := 1 end := -1 for i := start + index + 1; i < len(css); i++ { if css[i] == '}' { n-- if n == 0 { end = i + 1 if end < len(css) && css[end] == '\n' { end++ } break } } else if css[i] == '{' { n++ } } if end > index { session.animationCSS = strings.Trim(css[:index]+css[end:], "\n") if session.bridge != nil { session.bridge.setAnimationCSS(session.animationCSS) } else { ErrorLog("No connection") } } } func (session *sessionData) canvasStart(htmlID string) bool { if session.bridge != nil { session.bridge.canvasStart(htmlID) return true } ErrorLog("No connection") return false } func (session *sessionData) callCanvasFunc(funcName string, args ...any) { if session.bridge != nil { session.bridge.callCanvasFunc(funcName, args...) } } func (session *sessionData) updateCanvasProperty(property string, value any) { if session.bridge != nil { session.bridge.updateCanvasProperty(property, value) } } func (session *sessionData) createCanvasVar(funcName string, args ...any) any { if session.bridge != nil { return session.bridge.createCanvasVar(funcName, args...) } return nil } func (session *sessionData) createPath(arg string) any { if session.bridge != nil { return session.bridge.createPath2D(arg) } return nil } func (session *sessionData) callCanvasVarFunc(v any, funcName string, args ...any) { if session.bridge != nil && v != nil { session.bridge.callCanvasVarFunc(v, funcName, args...) } } func (session *sessionData) callCanvasImageFunc(url string, property string, funcName string, args ...any) { if session.bridge != nil { session.bridge.callCanvasImageFunc(url, property, funcName, args...) } } func (session *sessionData) canvasFinish() { if session.bridge != nil { session.bridge.canvasFinish() } } func (session *sessionData) canvasTextMetrics(htmlID, font, text string) TextMetrics { if session.bridge != nil { return session.bridge.canvasTextMetrics(htmlID, font, text) } ErrorLog("No connection") return TextMetrics{Width: 0} } func (session *sessionData) htmlPropertyValue(htmlID, name string) string { if session.bridge != nil { return session.bridge.htmlPropertyValue(htmlID, name) } ErrorLog("No connection") return "" } 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() } else { ErrorLog("No connection") } return true } func (session *sessionData) handleRootSize(data DataObject) { getValue := func(tag string) int { if value, ok := data.PropertyValue(tag); ok { float, err := strconv.ParseFloat(value, 64) if err == nil { return int(float) } ErrorLog(`Resize event error: ` + err.Error()) } else { ErrorLogF(`Resize event error: the property "%s" not found`, tag) } return 0 } if w := getValue("width"); w > 0 { session.screenWidth = w } if h := getValue("height"); h > 0 { session.screenHeight = h } } func (session *sessionData) handleResize(data DataObject) { if node := data.PropertyByTag("views"); node != nil && node.Type() == ArrayNode { for _, el := range node.ArrayElements() { if el.IsObject() { obj := el.Object() getFloat := func(tag string) float64 { if value, ok := obj.PropertyValue(tag); ok { f, err := strconv.ParseFloat(value, 64) if err == nil { return f } ErrorLog(`Resize event error: ` + err.Error()) } else { ErrorLogF(`Resize event error: the property "%s" not found`, tag) } return 0 } if viewID, ok := obj.PropertyValue("id"); ok { if n := strings.IndexRune(viewID, '-'); n > 0 { if view := session.viewByHTMLID(viewID[:n]); view != nil { view.onItemResize(view, viewID[n+1:], getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height")) } else { DebugLogF(`View with id == %s not found`, viewID[:n]) } } else if view := session.viewByHTMLID(viewID); view != nil { view.onResize(view, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height")) view.setScroll(getFloat("scroll-x"), getFloat("scroll-y"), getFloat("scroll-width"), getFloat("scroll-height")) } else { DebugLogF(`View with id == %s not found`, viewID) } } else { ErrorLog(`"id" property not found`) } } else { ErrorLog(`Resize event error: views element is not object`) } } } else { ErrorLog(`Resize event error: invalid "views" property`) } } func (session *sessionData) handleSessionInfo(params DataObject) { if value, ok := params.PropertyValue("touch"); ok { session.touchScreen = (value == "1" || value == "true") } if value, ok := params.PropertyValue("user-agent"); ok { session.userAgent = value } if value, ok := params.PropertyValue("direction"); ok { if value == "rtl" { session.textDirection = RightToLeftDirection } } if value, ok := params.PropertyValue("language"); ok { session.language = value } if value, ok := params.PropertyValue("languages"); ok { session.languages = strings.Split(value, ",") } if value, ok := params.PropertyValue("dark"); ok { session.darkTheme = (value == "1" || value == "true") } if value, ok := params.PropertyValue("pixel-ratio"); ok { if f, err := strconv.ParseFloat(value, 64); err != nil { ErrorLog(err.Error()) } else { session.pixelRatio = f } } if node := params.PropertyByTag("storage"); node != nil && node.Type() == ObjectNode { if obj := node.Object(); obj != nil { for i := 0; i < obj.PropertyCount(); i++ { if element := obj.Property(i); element.Type() == TextNode { session.clientStorage[element.Tag()] = element.Text() } } } } } func (session *sessionData) handleEvent(command string, data DataObject) { switch command { case "session-pause": session.onPause() 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) case "resize": session.handleResize(data) case "sessionInfo": session.handleSessionInfo(data) case "storageError": if text, ok := data.PropertyValue("error"); ok { ErrorLog(text) } default: if viewID, ok := data.PropertyValue("id"); ok { if viewID != "body" { if view := session.viewByHTMLID(viewID); view != nil { view.handleCommand(view, command, data) } } if command == KeyDownEvent { var event KeyEvent event.init(data) session.hotKey(event) } } else { ErrorLog(`"id" property not found. Event: ` + command) } } session.bridge.sendResponse() } func (session *sessionData) hotKey(event KeyEvent) { popups := session.popupManager().popups if count := len(popups); count > 0 { if popups[count-1].keyEvent(event) { return } } var controlKeys ControlKeyMask = 0 if event.AltKey { controlKeys |= AltKey } if event.CtrlKey { controlKeys |= CtrlKey } if event.MetaKey { controlKeys |= MetaKey } if event.ShiftKey { controlKeys |= ShiftKey } key := hotkeyCode(KeyCode(event.Code), controlKeys) if fn, ok := session.hotkeys[key]; ok && fn != nil { fn(session) } } func hotkeyCode(keyCode KeyCode, controlKeys ControlKeyMask) string { buffer := allocStringBuilder() defer freeStringBuilder(buffer) buffer.WriteString(strings.ToLower(string(keyCode))) if controlKeys != 0 { buffer.WriteRune('-') if controlKeys&AltKey != 0 { buffer.WriteRune('a') } if controlKeys&CtrlKey != 0 { buffer.WriteRune('c') } if controlKeys&MetaKey != 0 { buffer.WriteRune('m') } if controlKeys&ShiftKey != 0 { buffer.WriteRune('s') } } return buffer.String() } func (session *sessionData) SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session)) { hotkey := hotkeyCode(keyCode, controlKeys) if fn == nil { delete(session.hotkeys, hotkey) } else { session.hotkeys[hotkey] = fn } } func (session *sessionData) SetTitle(title string) { title, _ = session.GetString(title) session.callFunc("setTitle", title) } func (session *sessionData) SetTitleColor(color Color) { session.callFunc("setTitleColor", color.cssString()) } func (session *sessionData) RemoteAddr() string { return session.bridge.remoteAddr() } func (session *sessionData) OpenURL(urlStr string) { if _, err := url.ParseRequestURI(urlStr); err != nil { ErrorLog(err.Error()) return } session.callFunc("openURL", urlStr) } func (session *sessionData) ClientItem(key string) (string, bool) { value, ok := session.clientStorage[key] return value, ok } func (session *sessionData) SetClientItem(key, value string) { session.clientStorage[key] = value session.bridge.callFunc("localStorageSet", key, value) } func (session *sessionData) RemoveClientItem(key string) { delete(session.clientStorage, key) session.bridge.callFunc("localStorageRemove", key) } 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) } }