| /* |
| * Copyright (C) 2022 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "InspectorOverlayLabel.h" |
| |
| #include "FloatRoundedRect.h" |
| #include "FloatSize.h" |
| #include "FontCascade.h" |
| #include "FontCascadeDescription.h" |
| #include "GraphicsContext.h" |
| #include "Path.h" |
| |
| namespace WebCore { |
| |
| static constexpr float labelPadding = 4; |
| static constexpr float labelArrowSize = 6; |
| static constexpr float labelAdditionalLineSpacing = 1; |
| static constexpr float labelContentDecorationBorderedLeadingAndTrailingPadding = 1; |
| |
| InspectorOverlayLabel::InspectorOverlayLabel(Vector<Content>&& contents, FloatPoint location, Color backgroundColor, Arrow arrow) |
| : m_contents(WTFMove(contents)) |
| , m_location(location) |
| , m_backgroundColor(backgroundColor) |
| , m_arrow(arrow) |
| { |
| } |
| |
| InspectorOverlayLabel::InspectorOverlayLabel(const String& text, FloatPoint location, Color backgroundColor, Arrow arrow) |
| : InspectorOverlayLabel({ { text, Color::black } }, location, backgroundColor, arrow) |
| { |
| } |
| |
| static FontCascade systemFont() |
| { |
| FontCascadeDescription fontDescription; |
| fontDescription.setFamilies({ "system-ui"_s }); |
| fontDescription.setWeight(FontSelectionValue(500)); |
| fontDescription.setComputedSize(12); |
| |
| FontCascade font(WTFMove(fontDescription), 0, 0); |
| font.update(nullptr); |
| return font; |
| } |
| |
| static Path backgroundPath(float width, float height, InspectorOverlayLabel::Arrow arrow, float arrowSize) |
| { |
| Path path; |
| FloatSize offsetForArrowEdgePosition; |
| |
| switch (arrow.direction) { |
| case InspectorOverlayLabel::Arrow::Direction::Down: |
| path.moveTo({ -(width / 2), -height - arrowSize }); |
| path.addLineTo({ -(width / 2), -arrowSize }); |
| |
| switch (arrow.alignment) { |
| case InspectorOverlayLabel::Arrow::Alignment::Leading: |
| path.addLineTo({ -(width / 2), 0 }); |
| path.addLineTo({ -(width / 2) + arrowSize, -arrowSize }); |
| offsetForArrowEdgePosition = { (width / 2), 0 }; |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::Middle: |
| path.addLineTo({ -arrowSize, -arrowSize }); |
| path.addLineTo({ 0, 0 }); |
| path.addLineTo({ arrowSize, -arrowSize }); |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::Trailing: |
| path.addLineTo({ (width / 2) - arrowSize, -arrowSize }); |
| path.addLineTo({ (width / 2), 0 }); |
| offsetForArrowEdgePosition = { -(width / 2), 0 }; |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::None: |
| break; |
| } |
| |
| path.addLineTo({ (width / 2), -arrowSize }); |
| path.addLineTo({ (width / 2), -height - arrowSize }); |
| break; |
| case InspectorOverlayLabel::Arrow::Direction::Up: |
| path.moveTo({ -(width / 2), height + arrowSize }); |
| path.addLineTo({ -(width / 2), arrowSize }); |
| |
| switch (arrow.alignment) { |
| case InspectorOverlayLabel::Arrow::Alignment::Leading: |
| path.addLineTo({ -(width / 2), 0 }); |
| path.addLineTo({ -(width / 2) + arrowSize, arrowSize }); |
| offsetForArrowEdgePosition = { (width / 2), 0 }; |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::Middle: |
| path.addLineTo({ -arrowSize, arrowSize }); |
| path.addLineTo({ 0, 0 }); |
| path.addLineTo({ arrowSize, arrowSize }); |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::Trailing: |
| path.addLineTo({ (width / 2) - arrowSize, arrowSize }); |
| path.addLineTo({ (width / 2), 0 }); |
| offsetForArrowEdgePosition = { -(width / 2), 0 }; |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::None: |
| break; |
| } |
| |
| path.addLineTo({ (width / 2), arrowSize }); |
| path.addLineTo({ (width / 2), height + arrowSize }); |
| break; |
| case InspectorOverlayLabel::Arrow::Direction::Right: |
| path.moveTo({ -width - arrowSize, (height / 2) }); |
| path.addLineTo({ -arrowSize, (height / 2) }); |
| |
| switch (arrow.alignment) { |
| case InspectorOverlayLabel::Arrow::Alignment::Leading: |
| path.addLineTo({ -arrowSize, -(height / 2) + arrowSize }); |
| path.addLineTo({ 0, -(height / 2) }); |
| offsetForArrowEdgePosition = { 0, (height / 2) }; |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::Middle: |
| path.addLineTo({ -arrowSize, arrowSize }); |
| path.addLineTo({ 0, 0 }); |
| path.addLineTo({ -arrowSize, -arrowSize }); |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::Trailing: |
| path.addLineTo({ 0, (height / 2) }); |
| path.addLineTo({ -arrowSize, (height / 2) - arrowSize }); |
| offsetForArrowEdgePosition = { 0, -(height / 2) }; |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::None: |
| break; |
| } |
| |
| path.addLineTo({ -arrowSize, -(height / 2) }); |
| path.addLineTo({ -width - arrowSize, -(height / 2) }); |
| break; |
| case InspectorOverlayLabel::Arrow::Direction::Left: |
| path.moveTo({ width + arrowSize, (height / 2) }); |
| path.addLineTo({ arrowSize, (height / 2) }); |
| |
| switch (arrow.alignment) { |
| case InspectorOverlayLabel::Arrow::Alignment::Leading: |
| path.addLineTo({ arrowSize, -(height / 2) + arrowSize }); |
| path.addLineTo({ 0, -(height / 2) }); |
| offsetForArrowEdgePosition = { 0, (height / 2) }; |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::Middle: |
| path.addLineTo({ arrowSize, arrowSize }); |
| path.addLineTo({ 0, 0 }); |
| path.addLineTo({ arrowSize, -arrowSize }); |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::Trailing: |
| path.addLineTo({ 0, (height / 2) }); |
| path.addLineTo({ arrowSize, (height / 2) - arrowSize }); |
| offsetForArrowEdgePosition = { 0, -(height / 2) }; |
| break; |
| case InspectorOverlayLabel::Arrow::Alignment::None: |
| break; |
| } |
| |
| path.addLineTo({ arrowSize, -(height / 2) }); |
| path.addLineTo({ width + arrowSize, -(height / 2) }); |
| break; |
| case InspectorOverlayLabel::Arrow::Direction::None: |
| path.moveTo({ -(width / 2), -(height / 2) }); |
| path.addLineTo({ -(width / 2), height / 2 }); |
| path.addLineTo({ width / 2, height / 2 }); |
| path.addLineTo({ width / 2, -(height / 2) }); |
| break; |
| } |
| |
| path.closeSubpath(); |
| path.translate(offsetForArrowEdgePosition); |
| |
| return path; |
| } |
| |
| struct ComputedContentRun { |
| TextRun textRun; |
| Color textColor; |
| InspectorOverlayLabel::Content::Decoration decoration; |
| bool startsNewLine; |
| float computedWidth; |
| }; |
| |
| Path InspectorOverlayLabel::draw(GraphicsContext& context, float maximumLineWidth) |
| { |
| constexpr UChar ellipsis = 0x2026; |
| |
| auto font = systemFont(); |
| float lineHeight = font.metricsOfPrimaryFont().floatHeight(); |
| float lineDescent = font.metricsOfPrimaryFont().floatDescent(); |
| |
| Vector<ComputedContentRun> computedContentRuns; |
| |
| float longestLineWidth = 0; |
| int currentLine = 0; |
| |
| float currentLineWidth = 0; |
| for (auto content : m_contents) { |
| auto lines = content.text.splitAllowingEmptyEntries('\n'); |
| |
| ASSERT(content.decoration.type == Content::Decoration::Type::None || lines.size() <= 1); |
| |
| for (size_t i = 0; i < lines.size(); ++i) { |
| auto startsNewLine = !!i; |
| if (startsNewLine) { |
| currentLineWidth = 0; |
| ++currentLine; |
| } |
| |
| auto text = lines[i]; |
| auto textRun = TextRun(text); |
| float textWidth = font.width(textRun); |
| |
| // FIXME: This looks very inefficient. |
| if (maximumLineWidth && currentLineWidth + textWidth + (labelPadding * 2) > maximumLineWidth) { |
| text = makeString(text, ellipsis); |
| while (currentLineWidth + textWidth + (labelPadding * 2) > maximumLineWidth && text.length() > 1) { |
| // Remove the second from last character (the character before the ellipsis) and remeasure. |
| text = makeStringByRemoving(text, text.length() - 2, 1); |
| textRun = TextRun(text); |
| textWidth = font.width(textRun); |
| } |
| } |
| |
| computedContentRuns.append({ textRun, content.textColor, content.decoration, startsNewLine, textWidth }); |
| |
| currentLineWidth += textWidth; |
| if (currentLineWidth > longestLineWidth) |
| longestLineWidth = currentLineWidth; |
| } |
| } |
| |
| float totalTextHeight = (lineHeight * (currentLine + 1)) + (currentLine * labelAdditionalLineSpacing); |
| |
| FloatPoint textPosition; |
| switch (m_arrow.direction) { |
| case Arrow::Direction::Down: |
| switch (m_arrow.alignment) { |
| case Arrow::Alignment::Leading: |
| textPosition = FloatPoint(labelPadding, lineHeight - totalTextHeight - lineDescent - labelArrowSize - labelPadding); |
| break; |
| case Arrow::Alignment::Middle: |
| textPosition = FloatPoint(-(longestLineWidth / 2), lineHeight - totalTextHeight - lineDescent - labelArrowSize - labelPadding); |
| break; |
| case Arrow::Alignment::Trailing: |
| textPosition = FloatPoint(-longestLineWidth - labelPadding, lineHeight - totalTextHeight - lineDescent - labelArrowSize - labelPadding); |
| break; |
| case Arrow::Alignment::None: |
| break; |
| } |
| break; |
| case Arrow::Direction::Up: |
| switch (m_arrow.alignment) { |
| case Arrow::Alignment::Leading: |
| textPosition = FloatPoint(labelPadding, lineHeight - lineDescent + labelArrowSize + labelPadding); |
| break; |
| case Arrow::Alignment::Middle: |
| textPosition = FloatPoint(-(longestLineWidth / 2), lineHeight - lineDescent + labelArrowSize + labelPadding); |
| break; |
| case Arrow::Alignment::Trailing: |
| textPosition = FloatPoint(-longestLineWidth - labelPadding, lineHeight - lineDescent + labelArrowSize + labelPadding); |
| break; |
| case Arrow::Alignment::None: |
| break; |
| } |
| break; |
| case Arrow::Direction::Right: |
| switch (m_arrow.alignment) { |
| case Arrow::Alignment::Leading: |
| textPosition = FloatPoint(-longestLineWidth - labelArrowSize - labelPadding, labelPadding + lineHeight - lineDescent); |
| break; |
| case Arrow::Alignment::Middle: |
| textPosition = FloatPoint(-longestLineWidth - labelArrowSize - labelPadding, lineHeight - (totalTextHeight / 2) - lineDescent); |
| break; |
| case Arrow::Alignment::Trailing: |
| textPosition = FloatPoint(-longestLineWidth - labelArrowSize - labelPadding, lineHeight - totalTextHeight - labelPadding - lineDescent); |
| break; |
| case Arrow::Alignment::None: |
| break; |
| } |
| break; |
| case Arrow::Direction::Left: |
| switch (m_arrow.alignment) { |
| case Arrow::Alignment::Leading: |
| textPosition = FloatPoint(labelArrowSize + labelPadding, labelPadding + lineHeight - lineDescent); |
| break; |
| case Arrow::Alignment::Middle: |
| textPosition = FloatPoint(labelArrowSize + labelPadding, lineHeight - (totalTextHeight / 2) - lineDescent); |
| break; |
| case Arrow::Alignment::Trailing: |
| textPosition = FloatPoint(labelArrowSize + labelPadding, lineHeight - totalTextHeight - labelPadding - lineDescent); |
| break; |
| case Arrow::Alignment::None: |
| break; |
| } |
| break; |
| case Arrow::Direction::None: |
| textPosition = FloatPoint(-(longestLineWidth / 2), -(totalTextHeight / 2) + lineHeight - lineDescent); |
| break; |
| } |
| |
| Path labelPath = backgroundPath(longestLineWidth + (labelPadding * 2), totalTextHeight + (labelPadding * 2), m_arrow, labelArrowSize); |
| |
| GraphicsContextStateSaver saver(context); |
| context.translate(m_location); |
| |
| context.setFillColor(m_backgroundColor); |
| context.fillPath(labelPath); |
| context.strokePath(labelPath); |
| |
| int line = 0; |
| float xOffset = 0; |
| float yOffset = 0; |
| for (auto& computedContentRun : computedContentRuns) { |
| if (computedContentRun.startsNewLine) { |
| xOffset = 0; |
| ++line; |
| yOffset += lineHeight + labelAdditionalLineSpacing; |
| } |
| |
| switch (computedContentRun.decoration.type) { |
| case Content::Decoration::Type::Bordered: { |
| auto backgroundRect = FloatRoundedRect({ |
| textPosition.x() + xOffset - labelContentDecorationBorderedLeadingAndTrailingPadding, |
| textPosition.y() + yOffset - lineHeight + lineDescent, |
| computedContentRun.computedWidth + (labelContentDecorationBorderedLeadingAndTrailingPadding * 2), |
| lineHeight, |
| }, FloatRoundedRect::Radii(2)); |
| |
| Path backgroundPath; |
| backgroundPath.addRoundedRect(backgroundRect); |
| |
| context.setFillColor(computedContentRun.decoration.color); |
| context.setStrokeColor(computedContentRun.decoration.color.darkened()); |
| |
| context.fillPath(backgroundPath); |
| context.strokePath(backgroundPath); |
| break; |
| } |
| |
| case Content::Decoration::Type::None: |
| break; |
| } |
| |
| context.setFillColor(computedContentRun.textColor); |
| context.drawText(font, computedContentRun.textRun, textPosition + FloatPoint(xOffset, yOffset)); |
| |
| xOffset += computedContentRun.computedWidth; |
| } |
| |
| return labelPath; |
| } |
| |
| FloatSize InspectorOverlayLabel::expectedSize(const Vector<Content>& contents, Arrow::Direction direction) |
| { |
| auto font = systemFont(); |
| float lineHeight = font.metricsOfPrimaryFont().floatHeight(); |
| |
| float longestLineWidth = 0; |
| int currentLine = 0; |
| |
| float currentLineWidth = 0; |
| for (auto content : contents) { |
| auto lines = content.text.splitAllowingEmptyEntries('\n'); |
| for (size_t i = 0; i < lines.size(); ++i) { |
| if (i) { |
| currentLineWidth = 0; |
| ++currentLine; |
| } |
| |
| auto text = lines[i]; |
| if (text.isEmpty()) |
| continue; |
| |
| currentLineWidth += font.width(TextRun(text)); |
| if (currentLineWidth > longestLineWidth) |
| longestLineWidth = currentLineWidth; |
| } |
| } |
| |
| float totalTextHeight = (lineHeight * (currentLine + 1)) + (currentLine * labelAdditionalLineSpacing); |
| |
| switch (direction) { |
| case Arrow::Direction::Down: |
| case Arrow::Direction::Up: |
| return { longestLineWidth + (labelPadding * 2), totalTextHeight + (labelPadding * 2) + labelArrowSize }; |
| case Arrow::Direction::Right: |
| case Arrow::Direction::Left: |
| return { longestLineWidth + (labelPadding * 2) + labelArrowSize, totalTextHeight + (labelPadding * 2) }; |
| case Arrow::Direction::None: |
| return { longestLineWidth + (labelPadding * 2), totalTextHeight + (labelPadding * 2) }; |
| } |
| |
| RELEASE_ASSERT_NOT_REACHED(); |
| } |
| |
| FloatSize InspectorOverlayLabel::expectedSize(const String& text, Arrow::Direction direction) |
| { |
| return InspectorOverlayLabel::expectedSize({ { text, Color::black } }, direction); |
| } |
| |
| } // namespace WebCore |