From 51933508b990e0351033cf9541be0300b0d529b6 Mon Sep 17 00:00:00 2001 From: Jim Hague Date: Wed, 28 Jul 2021 11:30:32 +0100 Subject: [PATCH 1/7] Initial version of update border drawing. Minor adjustments to building the internal border-info property, and then convert View to use that property and the data therein to draw the border. We build a separate path for each border edge and draw these individually so a separate colour can be applied to each. Unfortunately anti-aliasing at the end of a path drawn by JUCE means that it looks like there's a small gap between lines. I don't have a good solution for that at present. --- packages/react-juce/src/lib/Backend.ts | 172 ++++++++++++++++ react_juce/core/View.cpp | 271 ++++++++++++++++++++----- react_juce/core/View.h | 7 +- 3 files changed, 398 insertions(+), 52 deletions(-) diff --git a/packages/react-juce/src/lib/Backend.ts b/packages/react-juce/src/lib/Backend.ts index 307054cf..bb92da3c 100644 --- a/packages/react-juce/src/lib/Backend.ts +++ b/packages/react-juce/src/lib/Backend.ts @@ -22,6 +22,78 @@ const cssPropsMap = allCssProps .filter((s) => !s.startsWith("-") && s.includes("-")) .reduce((acc, v) => Object.assign(acc, { [camelCase(v)]: v }), {}); +// known CSS border styles for React Native +const cssBorderStyles = ["dotted", "dashed", "solid"]; + +// Parse combination border properties. +function parseBorderSideProp(name: string, val: string | number, info: object) { + invariant( + typeof val === "string" || typeof val === "number", + name + " must be a string or a number" + ); + + if (typeof val === "number") + { + info[0] = info[1] = info[2] = info[3] = val; + return; + } + + const values = val.split(" "); + + invariant( + values.length >= 1 && values.length <= 4, + "border-" + name + " should be a space separated string with 1 to 4 values." + ); + + switch (values.length) { + case 1: + info[0] = info[1] = info[2] = info[3] = values[0]; + break; + case 2: + info[0] = info[2] = values[0]; + info[1] = info[3] = values[1]; + break; + case 3: + info[0] = values[0]; + info[1] = info[3] = values[1]; + info[2] = values[2]; + break; + default: + info[0] = values[0]; + info[1] = values[1]; + info[2] = values[2]; + info[3] = values[3]; + } +} + +function parseBorderProp(val: string, info: object) { + /* + Parameters can be in any order. So we need to recognised + which might be which. + */ + const numbers = "0123456789.-+"; + const values = val.split(" "); + + invariant( + values.length >= 1 && values.length <= 3, + "border should be a space separated string with 1 to 3 values." + ); + + const bs = info["style"]; + const bw = info["width"]; + const bc = info["color"]; + + for (val of values) { + if (cssBorderStyles.includes(val)) { + bs[0] = bs[1] = bs[2] = bs[3] = val; + } else if (numbers.includes(val.charAt(0))) { + bw[0] = bw[1] = bw[2] = bw[3] = val; + } else { + bc[0] = bc[1] = bc[2] = bc[3] = val; + } + } +} + export class ViewInstance { private _id: string; private _type: string; @@ -32,6 +104,12 @@ export class ViewInstance { constructor(id: string, type: string, props?: any, parent?: ViewInstance) { this._id = id; this._type = type; + this._border = { + "width": [0, 0, 0, 0], + "radius": ["0", "0", "0", "0"], + "color": ["", "", "", ""], + "style": ["solid", "solid", "solid", "solid"], + }; this._children = []; this._props = props; this._parent = parent; @@ -144,6 +222,100 @@ export class ViewInstance { NativeMethods.setViewProperty(this._id, k, v); return; } + + // Look for border properties and translate into our internal + // border-info property. + if (propKey.startsWith("border")) { + let gotBorderProp : boolean = true; + + switch (propKey) { + case "border": + parseBorderProp(value, this._border); + break; + + case "border-color": + parseBorderSideProp("color", value, this._border["color"]); + break; + case "border-top-color": + this._border["color"][0] = value; + break; + case "border-right-color": + this._border["color"][1] = value; + break; + case "border-bottom-color": + this._border["color"][2] = value; + break; + case "border-left-color": + this._border["color"][3] = value; + break; + + case "border-radius": + parseBorderSideProp("radius", value, this._border["radius"]); + break; + case "border-top-left-radius": + this._border["radius"][0] = value; + break; + case "border-top-right-radius": + this._border["radius"][1] = value; + break; + case "border-bottom-right-radius": + this._border["radius"][2] = value; + break; + case "border-bottom-left-radius": + this._border["radius"][3] = value; + break; + + case "border-style": + parseBorderSideProp("style", value, this._border["style"]); + break; + case "border-top-style": + this._border["style"][0] = value; + break; + case "border-right-style": + this._border["style"][1] = value; + break; + case "border-bottom-style": + this._border["style"][2] = value; + break; + case "border-left-style": + this._border["style"][3] = value; + break; + + case "border-width": + parseBorderSideProp("width", value, this._border["width"]); + break; + case "border-top-width": + this._border["width"][0] = value; + break; + case "border-right-width": + this._border["width"][1] = value; + break; + case "border-bottom-width": + this._border["width"][2] = value; + break; + case "border-left-width": + this._border["width"][3] = value; + break; + + default: + gotBorderProp = false; + break; + } + + invariant( + cssBorderStyles.includes(this._border["style"][0]) && + cssBorderStyles.includes(this._border["style"][1]) && + cssBorderStyles.includes(this._border["style"][2]) && + cssBorderStyles.includes(this._border["style"][3]), + "unknown border-style." + ); + + if (gotBorderProp) { + propKey = "border-info"; + value = this._border; + } + } + //@ts-ignore return NativeMethods.setViewProperty( this._id, diff --git a/react_juce/core/View.cpp b/react_juce/core/View.cpp index 9ec07350..38aac66a 100644 --- a/react_juce/core/View.cpp +++ b/react_juce/core/View.cpp @@ -12,6 +12,161 @@ #include +namespace { + enum BorderEdge + { + TOP = 0, + RIGHT, + BOTTOM, + LEFT + }; + + float getResolvedFloatProperty (const juce::var& val, float axisLength) + { + float ret; + + if (val.isString() && val.toString().trim().endsWithChar('%')) + { + float pctVal = val.toString().retainCharacters("-1234567890.").getFloatValue(); + ret = axisLength * (pctVal / 100.0f); + } + else + ret = static_cast(val); + + return ret; + } + + // Generate a path for a single border edge. With a path offset of + // 0, the path runs along the inner edge of the lines we draw, so it + // can be used to build a clip region for the content inside the border. + // The path offset pulls it out; we set that to half the line width to + // get a path in the middle of the line suitable to stroke to draw the border. + juce::Path makeEdgePath(BorderEdge edge, float width, float pathOffset, float prevWidth, float nextWidth, float startRadius, float endRadius, float borderWidth, float borderHeight) + { + juce::Path res; + const auto Pi = juce::MathConstants::pi; + juce::Point lineStart; + juce::Point lineEnd; + + if (startRadius > 0) + { + switch(edge) + { + case BorderEdge::TOP: + res.addCentredArc(prevWidth + startRadius, width + startRadius, + startRadius + pathOffset, startRadius + pathOffset, + 0, + 1.75 * Pi, 2.0 * Pi, + true); + break; + case BorderEdge::RIGHT: + res.addCentredArc(borderWidth - width - startRadius, prevWidth + startRadius, + startRadius + pathOffset, startRadius + pathOffset, + 0, + 0.25 * Pi, 0.5 * Pi, + true); + break; + case BorderEdge::BOTTOM: + res.addCentredArc(borderWidth - prevWidth - startRadius, borderHeight - width - startRadius, + startRadius + pathOffset, startRadius + pathOffset, + 0, + 0.75 * Pi, Pi, + true); + break; + case BorderEdge::LEFT: + res.addCentredArc(width + startRadius, borderHeight - prevWidth - startRadius, + startRadius + pathOffset, startRadius + pathOffset, + 0, + 1.25 * Pi, 1.5 * Pi, + true); + break; + } + } + + switch(edge) + { + case BorderEdge::TOP: + lineStart = juce::Point(prevWidth + startRadius, width - pathOffset); + break; + case BorderEdge::RIGHT: + lineStart = juce::Point(borderWidth - width + pathOffset, prevWidth + startRadius); + break; + case BorderEdge::BOTTOM: + lineStart = juce::Point(borderWidth - prevWidth - startRadius, borderHeight - width + pathOffset); + break; + case BorderEdge::LEFT: + lineStart = juce::Point(width - pathOffset, borderHeight - prevWidth - startRadius); + break; + } + + if (endRadius > 0) + { + switch(edge) + { + case BorderEdge::TOP: + res.addCentredArc(borderWidth - nextWidth - endRadius, width + endRadius, + endRadius + pathOffset, endRadius + pathOffset, + 0, + 0, 0.25 * Pi, + true); + lineEnd = juce::Point(borderWidth - prevWidth - endRadius, width - pathOffset); + break; + case BorderEdge::RIGHT: + res.addCentredArc(borderWidth - width - endRadius, borderHeight - nextWidth - endRadius, + endRadius + pathOffset, endRadius + pathOffset, + 0, + 0.5 * Pi, 0.75 * Pi, + true); + lineEnd = juce::Point(borderWidth - width + pathOffset, borderHeight - nextWidth - endRadius); + break; + case BorderEdge::BOTTOM: + res.addCentredArc(nextWidth + endRadius, borderHeight - width - endRadius, + endRadius + pathOffset, endRadius + pathOffset, + 0, + Pi, 1.25 * Pi, + true); + lineEnd = juce::Point(width + endRadius, borderHeight - width + pathOffset); + break; + case BorderEdge::LEFT: + res.addCentredArc(width + endRadius, nextWidth + endRadius, + endRadius + pathOffset, endRadius + pathOffset, + 0, + 1.5 * Pi, 1.75 * Pi, + true); + lineEnd = juce::Point(width - pathOffset, nextWidth + endRadius); + break; + } + } + else + { + // When joining no-radius corners, a browser will mitre the corners. + // For simplicity, we're taking an easier route - if we have a corner + // with two non-zero width borders, a designated border always 'wins'. + // The top border draw the top right corner, the right border the + // bottom right corner etc. + switch(edge) + { + case BorderEdge::TOP: + lineEnd = juce::Point(borderWidth, width - pathOffset); + break; + case BorderEdge::RIGHT: + lineEnd = juce::Point(borderWidth - width + pathOffset, borderHeight); + break; + case BorderEdge::BOTTOM: + lineEnd = juce::Point(0, borderHeight - width + pathOffset); + break; + case BorderEdge::LEFT: + lineEnd = juce::Point(width - pathOffset, 0); + break; + } + } + + res.startNewSubPath(lineStart); + res.lineTo(lineEnd); + + return res; + } +} namespace reactjuce { @@ -144,39 +299,19 @@ namespace reactjuce } //============================================================================== - float View::getResolvedLengthProperty (const juce::String& name, float axisLength) - { - float ret = 0; - - if (props.contains(name)) - { - const auto& v = props[name]; - - if (v.isString() && v.toString().trim().endsWithChar('%')) - { - float pctVal = v.toString().retainCharacters("-1234567890.").getFloatValue(); - ret = axisLength * (pctVal / 100.0f); - } - else - { - ret = (float) v; - } - } - - return ret; - } - void View::paint (juce::Graphics& g) { if (props.contains(borderPathProp)) { juce::Path p = juce::Drawable::parseSVGPath(props[borderPathProp].toString()); - if (props.contains(borderColorProp)) + if (props.contains(borderInfoProp)) { - juce::StringRef colorString = props[borderColorProp].toString(); + juce::StringRef colorString = props[borderInfoProp]["color"][0].toString(); juce::Colour c = juce::Colour::fromString(colorString); - float borderWidth = props.getWithDefault(borderWidthProp, 1.0); + float borderWidth = props[borderInfoProp]["width"][0]; + if (borderWidth == 0) + borderWidth = 1.0; g.setColour(c); g.strokePath(p, juce::PathStrokeType(borderWidth)); @@ -184,27 +319,71 @@ namespace reactjuce g.reduceClipRegion(p); } - else if (props.contains(borderColorProp) && props.contains(borderWidthProp)) + else if (props.contains(borderInfoProp)) { - juce::Path border; - juce::StringRef colorString = props[borderColorProp].toString(); - auto c = juce::Colour::fromString(colorString); - float borderWidth = props[borderWidthProp]; - - // Note this little bounds trick. When a Path is stroked, the line width extends - // outwards in both directions from the coordinate line. If the coordinate - // line is the exact bounding box then the component clipping makes the corners - // appear to have different radii on the interior and exterior of the box. - auto borderBounds = getLocalBounds().toFloat().reduced(borderWidth * 0.5f); - auto width = borderBounds.getWidth(); - auto height = borderBounds.getHeight(); - auto minLength = juce::jmin(width, height); - float borderRadius = getResolvedLengthProperty(borderRadiusProp.toString(), minLength); - - border.addRoundedRectangle(borderBounds, borderRadius); - g.setColour(c); - g.strokePath(border, juce::PathStrokeType(borderWidth)); - g.reduceClipRegion(border); + auto borderBounds = getLocalBounds().toFloat(); + auto borderWidth = borderBounds.getWidth(); + auto borderHeight = borderBounds.getHeight(); + auto minWidthHeight = juce::jmin(borderWidth, borderHeight); + float widths[LEFT + 1]; + float radii[LEFT + 1]; + juce::StringRef colors[LEFT + 1]; + + for (int edgeNo = TOP; edgeNo <= LEFT; ++edgeNo) + { + widths[edgeNo] = props[borderInfoProp]["width"][edgeNo]; + colors[edgeNo] = props[borderInfoProp]["color"][edgeNo].toString(); + radii[edgeNo] = getResolvedFloatProperty(props[borderInfoProp]["radius"][edgeNo], minWidthHeight); + } + + // Path and stroke the border edges. + for (int edgeNo = TOP; edgeNo <= LEFT; ++edgeNo) + { + float width = widths[edgeNo]; + if (width == 0) + continue; + + int prevEdgeNo = edgeNo == TOP ? LEFT : edgeNo - 1; + int nextEdgeNo = edgeNo == LEFT ? TOP : edgeNo + 1; + float prevWidth = widths[prevEdgeNo]; + float nextWidth = widths[nextEdgeNo]; + float startRadius = radii[edgeNo]; + float endRadius = radii[nextEdgeNo]; + + juce::Path p = makeEdgePath(static_cast(edgeNo), + width, width / 2, + prevWidth, nextWidth, + startRadius, endRadius, + borderWidth, borderHeight); + + auto color = juce::Colour::fromString(colors[edgeNo]); + g.setColour(color); + g.strokePath(p, juce::PathStrokeType(width)); + } + + // Path the clip region. + juce::Path clip; + for (int edgeNo = TOP; edgeNo <= LEFT; ++edgeNo) + { + float width = widths[edgeNo]; + if (width == 0) + continue; + + int prevEdgeNo = edgeNo == TOP ? LEFT : edgeNo - 1; + int nextEdgeNo = edgeNo == LEFT ? TOP : edgeNo + 1; + float prevWidth = widths[prevEdgeNo]; + float nextWidth = widths[nextEdgeNo]; + float startRadius = radii[edgeNo]; + float endRadius = radii[nextEdgeNo]; + + juce::Path p = makeEdgePath(static_cast(edgeNo), + width, 0, + prevWidth, nextWidth, + startRadius, endRadius, + borderWidth, borderHeight); + clip.addPath(p); + } + g.reduceClipRegion(clip); } if (props.contains(backgroundColorProp)) @@ -304,6 +483,6 @@ namespace reactjuce if (it != nativeMethods.end()) return it->second(args); - throw std::logic_error("Caller attempted to invoke a non-existent View method"); + throw std::logic_error(std::string("Caller attempted to invoke a non-existent View method ") + method.toStdString()); } } diff --git a/react_juce/core/View.h b/react_juce/core/View.h index 3473f8cc..ec7b66f2 100644 --- a/react_juce/core/View.h +++ b/react_juce/core/View.h @@ -38,10 +38,8 @@ namespace reactjuce static const inline juce::Identifier backgroundColorProp = "background-color"; - static const inline juce::Identifier borderColorProp = "border-color"; + static const inline juce::Identifier borderInfoProp = "border-info"; static const inline juce::Identifier borderPathProp = "border-path"; - static const inline juce::Identifier borderRadiusProp = "border-radius"; - static const inline juce::Identifier borderWidthProp = "border-width"; //============================================================================== View() = default; @@ -64,9 +62,6 @@ namespace reactjuce void setFloatBounds (juce::Rectangle bounds); //============================================================================== - /** Resolves a property to a specific point value or 0 if not present. */ - float getResolvedLengthProperty (const juce::String& name, float axisLength); - /** Override the default Component method with default paint behaviors. */ void paint (juce::Graphics& g) override; From f4ccf61e66c8b4ba66e0106a1dac80ff2e52bbf6 Mon Sep 17 00:00:00 2001 From: Jim Hague Date: Wed, 28 Jul 2021 14:49:18 +0100 Subject: [PATCH 2/7] Add implementation of dotted and dashed borders. Dotted borders seem to work as intended, but rendering of dashed borders is less certain. In both cases, changing the dash distances can result in bad rendering. I suspect a JUCE issue. --- react_juce/core/View.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/react_juce/core/View.cpp b/react_juce/core/View.cpp index 38aac66a..3690e280 100644 --- a/react_juce/core/View.cpp +++ b/react_juce/core/View.cpp @@ -328,12 +328,14 @@ namespace reactjuce float widths[LEFT + 1]; float radii[LEFT + 1]; juce::StringRef colors[LEFT + 1]; + juce::StringRef styles[LEFT + 1]; for (int edgeNo = TOP; edgeNo <= LEFT; ++edgeNo) { - widths[edgeNo] = props[borderInfoProp]["width"][edgeNo]; colors[edgeNo] = props[borderInfoProp]["color"][edgeNo].toString(); radii[edgeNo] = getResolvedFloatProperty(props[borderInfoProp]["radius"][edgeNo], minWidthHeight); + styles[edgeNo] = props[borderInfoProp]["style"][edgeNo].toString(); + widths[edgeNo] = props[borderInfoProp]["width"][edgeNo]; } // Path and stroke the border edges. @@ -358,7 +360,21 @@ namespace reactjuce auto color = juce::Colour::fromString(colors[edgeNo]); g.setColour(color); - g.strokePath(p, juce::PathStrokeType(width)); + auto strokeType = juce::PathStrokeType(width); + juce::Path strokedPath; + if (styles[edgeNo] == juce::String("dotted")) + { + float dash[] = { 1.0, 1.0 }; + strokeType.createDashedStroke(strokedPath, p, dash, 2); + } + else if (styles[edgeNo] == juce::String("dashed")) + { + float dash[] = { 4.0, 1.0 }; + strokeType.createDashedStroke(strokedPath, p, dash, 2); + } + else + strokeType.createStrokedPath(strokedPath, p); + g.fillPath(strokedPath); } // Path the clip region. From ae09281ec03689e0a6e252d928c20fd6383394f4 Mon Sep 17 00:00:00 2001 From: Jim Hague Date: Mon, 2 Aug 2021 15:35:47 +0100 Subject: [PATCH 3/7] Draw each border as a single path. --- react_juce/core/View.cpp | 59 ++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/react_juce/core/View.cpp b/react_juce/core/View.cpp index 3690e280..256b77b0 100644 --- a/react_juce/core/View.cpp +++ b/react_juce/core/View.cpp @@ -45,8 +45,6 @@ namespace { { juce::Path res; const auto Pi = juce::MathConstants::pi; - juce::Point lineStart; - juce::Point lineEnd; if (startRadius > 0) { @@ -82,21 +80,23 @@ namespace { break; } } - - switch(edge) + else { - case BorderEdge::TOP: - lineStart = juce::Point(prevWidth + startRadius, width - pathOffset); - break; - case BorderEdge::RIGHT: - lineStart = juce::Point(borderWidth - width + pathOffset, prevWidth + startRadius); - break; - case BorderEdge::BOTTOM: - lineStart = juce::Point(borderWidth - prevWidth - startRadius, borderHeight - width + pathOffset); - break; - case BorderEdge::LEFT: - lineStart = juce::Point(width - pathOffset, borderHeight - prevWidth - startRadius); - break; + switch(edge) + { + case BorderEdge::TOP: + res.startNewSubPath(prevWidth + startRadius, width - pathOffset); + break; + case BorderEdge::RIGHT: + res.startNewSubPath(borderWidth - width + pathOffset, prevWidth + startRadius); + break; + case BorderEdge::BOTTOM: + res.startNewSubPath(borderWidth - prevWidth - startRadius, borderHeight - width + pathOffset); + break; + case BorderEdge::LEFT: + res.startNewSubPath(width - pathOffset, borderHeight - prevWidth - startRadius); + break; + } } if (endRadius > 0) @@ -107,33 +107,25 @@ namespace { res.addCentredArc(borderWidth - nextWidth - endRadius, width + endRadius, endRadius + pathOffset, endRadius + pathOffset, 0, - 0, 0.25 * Pi, - true); - lineEnd = juce::Point(borderWidth - prevWidth - endRadius, width - pathOffset); + 0, 0.25 * Pi); break; case BorderEdge::RIGHT: res.addCentredArc(borderWidth - width - endRadius, borderHeight - nextWidth - endRadius, endRadius + pathOffset, endRadius + pathOffset, 0, - 0.5 * Pi, 0.75 * Pi, - true); - lineEnd = juce::Point(borderWidth - width + pathOffset, borderHeight - nextWidth - endRadius); + 0.5 * Pi, 0.75 * Pi); break; case BorderEdge::BOTTOM: res.addCentredArc(nextWidth + endRadius, borderHeight - width - endRadius, endRadius + pathOffset, endRadius + pathOffset, 0, - Pi, 1.25 * Pi, - true); - lineEnd = juce::Point(width + endRadius, borderHeight - width + pathOffset); + Pi, 1.25 * Pi); break; case BorderEdge::LEFT: res.addCentredArc(width + endRadius, nextWidth + endRadius, endRadius + pathOffset, endRadius + pathOffset, 0, - 1.5 * Pi, 1.75 * Pi, - true); - lineEnd = juce::Point(width - pathOffset, nextWidth + endRadius); + 1.5 * Pi, 1.75 * Pi); break; } } @@ -147,23 +139,20 @@ namespace { switch(edge) { case BorderEdge::TOP: - lineEnd = juce::Point(borderWidth, width - pathOffset); + res.lineTo(borderWidth, width - pathOffset); break; case BorderEdge::RIGHT: - lineEnd = juce::Point(borderWidth - width + pathOffset, borderHeight); + res.lineTo(borderWidth - width + pathOffset, borderHeight); break; case BorderEdge::BOTTOM: - lineEnd = juce::Point(0, borderHeight - width + pathOffset); + res.lineTo(0, borderHeight - width + pathOffset); break; case BorderEdge::LEFT: - lineEnd = juce::Point(width - pathOffset, 0); + res.lineTo(width - pathOffset, 0); break; } } - res.startNewSubPath(lineStart); - res.lineTo(lineEnd); - return res; } } From 6748280eed1d4d62978eb8fa308b915675e6b9bc Mon Sep 17 00:00:00 2001 From: Jim Hague Date: Mon, 2 Aug 2021 16:09:57 +0100 Subject: [PATCH 4/7] Ensure corner horizontal/vertical radiuses do not exceed half the area width or height. --- react_juce/core/View.cpp | 48 ++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/react_juce/core/View.cpp b/react_juce/core/View.cpp index 256b77b0..f8be176e 100644 --- a/react_juce/core/View.cpp +++ b/react_juce/core/View.cpp @@ -51,29 +51,37 @@ namespace { switch(edge) { case BorderEdge::TOP: - res.addCentredArc(prevWidth + startRadius, width + startRadius, - startRadius + pathOffset, startRadius + pathOffset, + res.addCentredArc(juce::jmin(prevWidth + startRadius, borderWidth / 2), + juce::jmin(width + startRadius, borderHeight / 2), + juce::jmin(startRadius, borderWidth / 2 - pathOffset) + pathOffset, + juce::jmin(startRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, 1.75 * Pi, 2.0 * Pi, true); break; case BorderEdge::RIGHT: - res.addCentredArc(borderWidth - width - startRadius, prevWidth + startRadius, - startRadius + pathOffset, startRadius + pathOffset, + res.addCentredArc(juce::jmax(borderWidth - width - startRadius, borderWidth / 2), + juce::jmin(prevWidth + startRadius, borderHeight / 2), + juce::jmin(startRadius, borderWidth / 2 - pathOffset) + pathOffset, + juce::jmin(startRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, 0.25 * Pi, 0.5 * Pi, true); break; case BorderEdge::BOTTOM: - res.addCentredArc(borderWidth - prevWidth - startRadius, borderHeight - width - startRadius, - startRadius + pathOffset, startRadius + pathOffset, + res.addCentredArc(juce::jmax(borderWidth - prevWidth - startRadius, borderWidth / 2), + juce::jmax(borderHeight - width - startRadius, borderHeight / 2), + juce::jmin(startRadius, borderWidth / 2 - pathOffset) + pathOffset, + juce::jmin(startRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, 0.75 * Pi, Pi, true); break; case BorderEdge::LEFT: - res.addCentredArc(width + startRadius, borderHeight - prevWidth - startRadius, - startRadius + pathOffset, startRadius + pathOffset, + res.addCentredArc(juce::jmin(width + startRadius, borderWidth / 2), + juce::jmax(borderHeight - prevWidth - startRadius, borderHeight / 2), + juce::jmin(startRadius, borderWidth / 2 - pathOffset) + pathOffset, + juce::jmin(startRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, 1.25 * Pi, 1.5 * Pi, true); @@ -104,26 +112,34 @@ namespace { switch(edge) { case BorderEdge::TOP: - res.addCentredArc(borderWidth - nextWidth - endRadius, width + endRadius, - endRadius + pathOffset, endRadius + pathOffset, + res.addCentredArc(juce::jmax(borderWidth - nextWidth - endRadius, borderWidth / 2), + juce::jmin(width + endRadius, borderHeight / 2), + juce::jmin(endRadius, borderWidth / 2 - pathOffset) + pathOffset, + juce::jmin(endRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, 0, 0.25 * Pi); break; case BorderEdge::RIGHT: - res.addCentredArc(borderWidth - width - endRadius, borderHeight - nextWidth - endRadius, - endRadius + pathOffset, endRadius + pathOffset, + res.addCentredArc(juce::jmax(borderWidth - width - endRadius, borderWidth / 2), + juce::jmax(borderHeight - nextWidth - endRadius, borderHeight / 2), + juce::jmin(endRadius, borderWidth / 2 - pathOffset) + pathOffset, + juce::jmin(endRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, 0.5 * Pi, 0.75 * Pi); break; case BorderEdge::BOTTOM: - res.addCentredArc(nextWidth + endRadius, borderHeight - width - endRadius, - endRadius + pathOffset, endRadius + pathOffset, + res.addCentredArc(juce::jmin(nextWidth + endRadius, borderWidth / 2), + juce::jmax(borderHeight - width - endRadius, borderHeight / 2), + juce::jmin(endRadius, borderWidth / 2 - pathOffset) + pathOffset, + juce::jmin(endRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, Pi, 1.25 * Pi); break; case BorderEdge::LEFT: - res.addCentredArc(width + endRadius, nextWidth + endRadius, - endRadius + pathOffset, endRadius + pathOffset, + res.addCentredArc(juce::jmin(width + endRadius, borderWidth / 2), + juce::jmin(nextWidth + endRadius, borderHeight / 2), + juce::jmin(endRadius, borderWidth / 2 - pathOffset) + pathOffset, + juce::jmin(endRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, 1.5 * Pi, 1.75 * Pi); break; From bf6d4068b949c099401b7891098160a9ab618e0d Mon Sep 17 00:00:00 2001 From: Jim Hague Date: Tue, 21 Sep 2021 12:10:41 +0100 Subject: [PATCH 5/7] Add border-(top|bottom|left|right)-color to the list of colour properties This fixes borders not being drawn when only the individual side colours were specified. --- packages/react-juce/src/lib/MacroProperties/Colors.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/react-juce/src/lib/MacroProperties/Colors.ts b/packages/react-juce/src/lib/MacroProperties/Colors.ts index 150fde68..c6573107 100644 --- a/packages/react-juce/src/lib/MacroProperties/Colors.ts +++ b/packages/react-juce/src/lib/MacroProperties/Colors.ts @@ -1,7 +1,15 @@ import ColorString from "color-string"; import ColorNames from "color-name"; -const COLOR_PROPERTIES = ["border-color", "background-color", "color"]; +const COLOR_PROPERTIES = [ + "border-color", + "border-top-color", + "border-bottom-color", + "border-left-color", + "border-right-color", + "background-color", + "color", +]; const isColorProperty = (propKey: string): boolean => { return COLOR_PROPERTIES.includes(propKey); From 8b63578f07510095e2a1c0b20ef976f1c64d975e Mon Sep 17 00:00:00 2001 From: David Stephenson Date: Mon, 20 Sep 2021 15:56:46 +0100 Subject: [PATCH 6/7] [build] Fix breakages in View.cpp due to failed conversions double->float --- react_juce/core/View.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/react_juce/core/View.cpp b/react_juce/core/View.cpp index f8be176e..ba1be7eb 100644 --- a/react_juce/core/View.cpp +++ b/react_juce/core/View.cpp @@ -56,7 +56,7 @@ namespace { juce::jmin(startRadius, borderWidth / 2 - pathOffset) + pathOffset, juce::jmin(startRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, - 1.75 * Pi, 2.0 * Pi, + 1.75f * Pi, 2.0f * Pi, true); break; case BorderEdge::RIGHT: @@ -65,7 +65,7 @@ namespace { juce::jmin(startRadius, borderWidth / 2 - pathOffset) + pathOffset, juce::jmin(startRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, - 0.25 * Pi, 0.5 * Pi, + 0.25f * Pi, 0.5f * Pi, true); break; case BorderEdge::BOTTOM: @@ -74,7 +74,7 @@ namespace { juce::jmin(startRadius, borderWidth / 2 - pathOffset) + pathOffset, juce::jmin(startRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, - 0.75 * Pi, Pi, + 0.75f * Pi, Pi, true); break; case BorderEdge::LEFT: @@ -83,7 +83,7 @@ namespace { juce::jmin(startRadius, borderWidth / 2 - pathOffset) + pathOffset, juce::jmin(startRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, - 1.25 * Pi, 1.5 * Pi, + 1.25f * Pi, 1.5f * Pi, true); break; } @@ -117,7 +117,7 @@ namespace { juce::jmin(endRadius, borderWidth / 2 - pathOffset) + pathOffset, juce::jmin(endRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, - 0, 0.25 * Pi); + 0, 0.25f * Pi); break; case BorderEdge::RIGHT: res.addCentredArc(juce::jmax(borderWidth - width - endRadius, borderWidth / 2), @@ -125,7 +125,7 @@ namespace { juce::jmin(endRadius, borderWidth / 2 - pathOffset) + pathOffset, juce::jmin(endRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, - 0.5 * Pi, 0.75 * Pi); + 0.5f * Pi, 0.75f * Pi); break; case BorderEdge::BOTTOM: res.addCentredArc(juce::jmin(nextWidth + endRadius, borderWidth / 2), @@ -133,7 +133,7 @@ namespace { juce::jmin(endRadius, borderWidth / 2 - pathOffset) + pathOffset, juce::jmin(endRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, - Pi, 1.25 * Pi); + Pi, 1.25f * Pi); break; case BorderEdge::LEFT: res.addCentredArc(juce::jmin(width + endRadius, borderWidth / 2), @@ -141,7 +141,7 @@ namespace { juce::jmin(endRadius, borderWidth / 2 - pathOffset) + pathOffset, juce::jmin(endRadius, borderHeight / 2 - pathOffset) + pathOffset, 0, - 1.5 * Pi, 1.75 * Pi); + 1.5f * Pi, 1.75f * Pi); break; } } From dcffb7dcc32961060c7365fd51d46de3eb4d6f96 Mon Sep 17 00:00:00 2001 From: Jim Hague Date: Tue, 21 Sep 2021 17:29:28 +0100 Subject: [PATCH 7/7] Code style fixups and add explicit BorderInfo type Placate prettier. --- packages/react-juce/src/lib/Backend.ts | 38 +++++++++++++++++--------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/react-juce/src/lib/Backend.ts b/packages/react-juce/src/lib/Backend.ts index bb92da3c..43338966 100644 --- a/packages/react-juce/src/lib/Backend.ts +++ b/packages/react-juce/src/lib/Backend.ts @@ -7,10 +7,18 @@ import SyntheticEvents, { } from "./SyntheticEvents"; import { macroPropertyGetters } from "./MacroProperties"; import Colors from "./MacroProperties/Colors"; +import invariant from "invariant"; //TODO: Keep this union or introduce a common base class ViewInstanceBase? export type Instance = ViewInstance | RawTextViewInstance; +type BorderInfo = { + width: number[]; + radius: string[]; + color: string[]; + style: string[]; +}; + let __rootViewInstance: ViewInstance | null = null; let __viewRegistry: Map = new Map(); let __lastMouseDownViewId: string | null = null; @@ -26,15 +34,18 @@ const cssPropsMap = allCssProps const cssBorderStyles = ["dotted", "dashed", "solid"]; // Parse combination border properties. -function parseBorderSideProp(name: string, val: string | number, info: object) { +function parseBorderSideProp( + name: string, + val: string | number, + info: string[] | number[] +) { invariant( typeof val === "string" || typeof val === "number", name + " must be a string or a number" ); - if (typeof val === "number") - { - info[0] = info[1] = info[2] = info[3] = val; + if (typeof val === "number") { + info[0] = info[1] = info[2] = info[3] = +val; return; } @@ -87,7 +98,7 @@ function parseBorderProp(val: string, info: object) { if (cssBorderStyles.includes(val)) { bs[0] = bs[1] = bs[2] = bs[3] = val; } else if (numbers.includes(val.charAt(0))) { - bw[0] = bw[1] = bw[2] = bw[3] = val; + bw[0] = bw[1] = bw[2] = bw[3] = +val; } else { bc[0] = bc[1] = bc[2] = bc[3] = val; } @@ -97,6 +108,7 @@ function parseBorderProp(val: string, info: object) { export class ViewInstance { private _id: string; private _type: string; + private _border: BorderInfo; public _children: Instance[]; public _props: any = null; public _parent: any = null; @@ -105,10 +117,10 @@ export class ViewInstance { this._id = id; this._type = type; this._border = { - "width": [0, 0, 0, 0], - "radius": ["0", "0", "0", "0"], - "color": ["", "", "", ""], - "style": ["solid", "solid", "solid", "solid"], + width: [0, 0, 0, 0], + radius: ["0", "0", "0", "0"], + color: ["", "", "", ""], + style: ["solid", "solid", "solid", "solid"], }; this._children = []; this._props = props; @@ -226,7 +238,7 @@ export class ViewInstance { // Look for border properties and translate into our internal // border-info property. if (propKey.startsWith("border")) { - let gotBorderProp : boolean = true; + let gotBorderProp: boolean = true; switch (propKey) { case "border": @@ -304,9 +316,9 @@ export class ViewInstance { invariant( cssBorderStyles.includes(this._border["style"][0]) && - cssBorderStyles.includes(this._border["style"][1]) && - cssBorderStyles.includes(this._border["style"][2]) && - cssBorderStyles.includes(this._border["style"][3]), + cssBorderStyles.includes(this._border["style"][1]) && + cssBorderStyles.includes(this._border["style"][2]) && + cssBorderStyles.includes(this._border["style"][3]), "unknown border-style." );