diff --git a/CHANGELOG.md b/CHANGELOG.md index c87de36..7676c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v.10.0 + +* The Canvas.TextWidth method replaced by Canvas.TextMetrics + # v0.9.0 * Requires go 1.18 or higher diff --git a/app_scripts.js b/app_scripts.js index ff52a69..f6fdbe9 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -1702,3 +1702,44 @@ function imageError(element, event) { var message = "imageViewError{session=" + sessionID + ",id=" + element.id + "}"; sendMessage(message); } + +function canvasTextMetrics(answerID, elementId, font, text) { + var w = 0; + var ascent = 0; + var descent = 0; + var left = 0; + var right = 0; + + const canvas = document.getElementById(elementId); + if (canvas) { + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.save() + const dpr = window.devicePixelRatio || 1; + ctx.scale(dpr, dpr); + ctx.font = font; + ctx.textBaseline = 'alphabetic'; + ctx.textAlign = 'start'; + var metrics = ctx.measureText(text) + w = metrics.width; + ascent = metrics.actualBoundingBoxAscent; + descent = metrics.actualBoundingBoxDescent; + left = metrics.actualBoundingBoxLeft; + right = metrics.actualBoundingBoxRight; + ctx.restore(); + } + } + + sendMessage('answer{answerID=' + answerID + ', width=' + w + ', ascent=' + ascent + + ', descent=' + descent + ', left=' + left + ', right=' + right + '}'); +} + +function getPropertyValue(answerID, elementId, name) { + const element = document.getElementById(elementId); + if (element && element[name]) { + sendMessage('answer{answerID=' + answerID + ', value="' + element[name] + '"}') + return + } + + sendMessage('answer{answerID=' + answerID + ', value=""}') +} \ No newline at end of file diff --git a/canvas.go b/canvas.go index d1c4df8..2489829 100644 --- a/canvas.go +++ b/canvas.go @@ -76,6 +76,21 @@ type FontParams struct { LineHeight SizeUnit } +// TextMetrics is the result of the Canvas.TextMetrics function +type TextMetrics struct { + // Width is the calculated width of a segment of inline text in pixels + Width float64 + // Ascent is the distance from the horizontal baseline to the top of the bounding rectangle used to render the text, in pixels. + Ascent float64 + // Descent is the distance from the horizontal baseline to the bottom of the bounding rectangle used to render the text, in pixels. + Descent float64 + // Left is the distance to the left side of the bounding rectangle of the given text, in pixels; + // positive numbers indicating a distance going left from the given alignment point. + Left float64 + // Right is the distance to the right side of the bounding rectangle of the given text, CSS pixels. + Right float64 +} + // Canvas is a drawing interface type Canvas interface { // View return the view for the drawing @@ -195,8 +210,8 @@ type Canvas interface { // SetFontWithParams sets the current text style to use when drawing text SetFontWithParams(name string, size SizeUnit, params FontParams) - // TextWidth calculates the width of the text drawn by a given font - TextWidth(text string, fontName string, fontSize SizeUnit) float64 + // TextWidth calculates metrics of the text drawn by a given font + TextMetrics(text string, fontName string, fontSize SizeUnit, fontParams FontParams) TextMetrics // SetTextBaseline sets the current text baseline used when drawing text. Valid values: // AlphabeticBaseline (0), TopBaseline (1), MiddleBaseline (2), BottomBaseline (3), @@ -596,6 +611,67 @@ func (canvas *canvasData) SetFont(name string, size SizeUnit) { canvas.writeFont(name, &canvas.script) } +func (canvas *canvasData) fontWithParams(name string, size SizeUnit, params FontParams) string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + if params.Italic { + buffer.WriteString("italic ") + } + if params.SmallCaps { + buffer.WriteString("small-caps ") + } + if params.Weight > 0 && params.Weight <= 9 { + switch params.Weight { + case 4: + buffer.WriteString("normal ") + case 7: + buffer.WriteString("bold ") + default: + buffer.WriteString(strconv.Itoa(params.Weight * 100)) + buffer.WriteRune(' ') + } + } + + buffer.WriteString(size.cssString("1rem", canvas.View().Session())) + switch params.LineHeight.Type { + case Auto: + + case SizeInPercent: + if params.LineHeight.Value != 100 { + buffer.WriteString("/") + buffer.WriteString(strconv.FormatFloat(params.LineHeight.Value/100, 'g', -1, 64)) + } + + case SizeInFraction: + if params.LineHeight.Value != 1 { + buffer.WriteString("/") + buffer.WriteString(strconv.FormatFloat(params.LineHeight.Value, 'g', -1, 64)) + } + + default: + buffer.WriteString("/") + buffer.WriteString(params.LineHeight.cssString("", canvas.View().Session())) + } + + names := strings.Split(name, ",") + lead := " " + for _, font := range names { + font = strings.Trim(font, " \n\"'") + buffer.WriteString(lead) + lead = "," + if strings.Contains(font, " ") { + buffer.WriteRune('"') + buffer.WriteString(font) + buffer.WriteRune('"') + } else { + buffer.WriteString(font) + } + } + + return buffer.String() +} + func (canvas *canvasData) setFontWithParams(name string, size SizeUnit, params FontParams, script *strings.Builder) { script.WriteString("\nctx.font = '") if params.Italic { @@ -644,57 +720,9 @@ func (canvas *canvasData) SetFontWithParams(name string, size SizeUnit, params F canvas.setFontWithParams(name, size, params, &canvas.script) } -func (canvas *canvasData) TextWidth(text string, fontName string, fontSize SizeUnit) float64 { - buffer := allocStringBuilder() - defer freeStringBuilder(buffer) - - canvas.setFontWithParams(fontName, fontSize, FontParams{}, buffer) - fontParams := buffer.String() - - buffer.Reset() - canvas.writeStringArgs(text, buffer) - str := buffer.String() - - script := fmt.Sprintf(` -var w = 0; -const canvas = document.getElementById('%s'); -if (canvas) { - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.save() - const dpr = window.devicePixelRatio || 1; - ctx.scale(dpr, dpr); - %s; - w = ctx.measureText('%s').width; - ctx.restore(); - } -} -sendMessage('answer{width=' + w + ', answerID=' + answerID + '}'); -`, canvas.View().htmlID(), fontParams, str) - - result := canvas.View().Session().runGetterScript(script) - switch result.Tag() { - case "answer": - if value, ok := result.PropertyValue("width"); ok { - w, err := strconv.ParseFloat(value, 32) - if err == nil { - return w - } - ErrorLog(err.Error()) - } - - case "error": - if text, ok := result.PropertyValue("errorText"); ok { - ErrorLog(text) - } else { - ErrorLog("error") - } - - default: - ErrorLog("Unknown answer: " + result.Tag()) - } - - return 0 +func (canvas *canvasData) TextMetrics(text string, fontName string, fontSize SizeUnit, fontParams FontParams) TextMetrics { + view := canvas.View() + return view.Session().canvasTextMetrics(view.htmlID(), canvas.fontWithParams(fontName, fontSize, fontParams), text) } func (canvas *canvasData) SetTextBaseline(baseline int) { diff --git a/mediaPlayer.go b/mediaPlayer.go index 03cef77..26979a0 100644 --- a/mediaPlayer.go +++ b/mediaPlayer.go @@ -667,48 +667,15 @@ func (player *mediaPlayerData) SetCurrentTime(seconds float64) { } func (player *mediaPlayerData) getFloatPlayerProperty(tag string) (float64, bool) { - - script := allocStringBuilder() - defer freeStringBuilder(script) - - script.WriteString(`const element = document.getElementById('`) - script.WriteString(player.htmlID()) - script.WriteString(`'); -if (element && element.`) - script.WriteString(tag) - script.WriteString(`) { - sendMessage('answer{answerID=' + answerID + ',`) - script.WriteString(tag) - script.WriteString(`=' + element.`) - script.WriteString(tag) - script.WriteString(` + '}'); -} else { - sendMessage('answer{answerID=' + answerID + ',`) - script.WriteString(tag) - script.WriteString(`=0}'); -}`) - - result := player.Session().runGetterScript(script.String()) - switch result.Tag() { - case "answer": - if value, ok := result.PropertyValue(tag); ok { - w, err := strconv.ParseFloat(value, 32) - if err == nil { - return w, true - } - ErrorLog(err.Error()) + value := player.Session().htmlPropertyValue(player.htmlID(), tag) + if value != "" { + result, err := strconv.ParseFloat(value, 32) + if err == nil { + return result, true } - - case "error": - if text, ok := result.PropertyValue("errorText"); ok { - ErrorLog(text) - } else { - ErrorLog("error") - } - - default: - ErrorLog("Unknown answer: " + result.Tag()) + ErrorLog(err.Error()) } + return 0, false } @@ -751,45 +718,14 @@ func (player *mediaPlayerData) Volume() float64 { } func (player *mediaPlayerData) getBoolPlayerProperty(tag string) (bool, bool) { + switch value := player.Session().htmlPropertyValue(player.htmlID(), tag); strings.ToLower(value) { + case "0", "false", "off": + return false, true - script := allocStringBuilder() - defer freeStringBuilder(script) - - script.WriteString(`const element = document.getElementById('`) - script.WriteString(player.htmlID()) - script.WriteString(`'); -if (element && element.`) - script.WriteString(tag) - script.WriteString(`) { - sendMessage('answer{answerID=' + answerID + ',`) - script.WriteString(tag) - script.WriteString(`=1}') -} else { - sendMessage('answer{answerID=' + answerID + ',`) - script.WriteString(tag) - script.WriteString(`=0}') -}`) - - result := player.Session().runGetterScript(script.String()) - switch result.Tag() { - case "answer": - if value, ok := result.PropertyValue(tag); ok { - if value == "1" { - return true, true - } - return false, true - } - - case "error": - if text, ok := result.PropertyValue("errorText"); ok { - ErrorLog(text) - } else { - ErrorLog("error") - } - - default: - ErrorLog("Unknown answer: " + result.Tag()) + case "1", "true", "on": + return false, true } + return false, false } diff --git a/session.go b/session.go index 5a8f510..01248ba 100644 --- a/session.go +++ b/session.go @@ -11,7 +11,8 @@ type webBrige interface { runFunc(funcName string, args ...any) bool readMessage() (string, bool) writeMessage(text string) bool - runGetterScript(script string) DataObject + canvasTextMetrics(htmlID, font, text string) TextMetrics + htmlPropertyValue(htmlID, name string) string answerReceived(answer DataObject) close() remoteAddr() string @@ -99,7 +100,8 @@ type Session interface { writeInitScript(writer *strings.Builder) runFunc(funcName string, args ...any) runScript(script string) - runGetterScript(script string) DataObject //, answer chan DataObject) + canvasTextMetrics(htmlID, font, text string) TextMetrics + htmlPropertyValue(htmlID, name string) string handleAnswer(data DataObject) handleRootSize(data DataObject) handleResize(data DataObject) @@ -339,15 +341,22 @@ func (session *sessionData) runScript(script string) { } } -func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) { +func (session *sessionData) canvasTextMetrics(htmlID, font, text string) TextMetrics { if session.brige != nil { - return session.brige.runGetterScript(script) + return session.brige.canvasTextMetrics(htmlID, font, text) } ErrorLog("No connection") - result := NewDataObject("error") - result.SetPropertyValue("text", "No connection") - return result + return TextMetrics{Width: 0} +} + +func (session *sessionData) htmlPropertyValue(htmlID, name string) string { + if session.brige != nil { + return session.brige.htmlPropertyValue(htmlID, name) + } + + ErrorLog("No connection") + return "" } func (session *sessionData) handleAnswer(data DataObject) { diff --git a/webBrige.go b/webBrige.go index 20f23a6..ab33ace 100644 --- a/webBrige.go +++ b/webBrige.go @@ -151,6 +151,10 @@ func (brige *wsBrige) writeMessage(script string) bool { DebugLog("Run script:") DebugLog(script) } + if brige.conn == nil { + ErrorLog("No connection") + return false + } if err := brige.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil { ErrorLog(err.Error()) return false @@ -158,7 +162,9 @@ func (brige *wsBrige) writeMessage(script string) bool { return true } -func (brige *wsBrige) runGetterScript(script string) DataObject { +func (brige *wsBrige) canvasTextMetrics(htmlID, font, text string) TextMetrics { + result := TextMetrics{} + brige.answerMutex.Lock() answerID := brige.answerID brige.answerID++ @@ -166,30 +172,36 @@ func (brige *wsBrige) runGetterScript(script string) DataObject { answer := make(chan DataObject) brige.answer[answerID] = answer - errorText := "" - if brige.conn != nil { - script = "var answerID = " + strconv.Itoa(answerID) + ";\n" + script - if ProtocolInDebugLog { - DebugLog("\n" + script) - } - err := brige.conn.WriteMessage(websocket.TextMessage, []byte(script)) - if err == nil { - return <-answer - } - errorText = err.Error() - } else { - if ProtocolInDebugLog { - DebugLog("\n" + script) - } - errorText = "No connection" + + if brige.runFunc("canvasTextMetrics", answerID, htmlID, font, text) { + data := <-answer + result.Width = dataFloatProperty(data, "width") } - result := NewDataObject("error") - result.SetPropertyValue("text", errorText) delete(brige.answer, answerID) return result } +func (brige *wsBrige) htmlPropertyValue(htmlID, name string) string { + brige.answerMutex.Lock() + answerID := brige.answerID + brige.answerID++ + brige.answerMutex.Unlock() + + answer := make(chan DataObject) + brige.answer[answerID] = answer + + if brige.runFunc("getPropertyValue", answerID, htmlID, name) { + data := <-answer + if value, ok := data.PropertyValue("value"); ok { + return value + } + } + + delete(brige.answer, answerID) + return "" +} + func (brige *wsBrige) answerReceived(answer DataObject) { if text, ok := answer.PropertyValue("answerID"); ok { if id, err := strconv.Atoi(text); err == nil {