blob: 30f42c3ecf39644dc2c31f34dd1aaf5e6ce801f1 [file] [log] [blame]
/*
* Copyright (C) 2022-2023 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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 "AttachmentLayout.h"
#if ENABLE(ATTACHMENT_ELEMENT) && PLATFORM(COCOA)
#include "ColorCocoa.h"
#include "ElementInlines.h"
#include "FontCacheCoreText.h"
#include "FrameSelection.h"
#include "GeometryUtilities.h"
#include "RenderObjectInlines.h"
#include "RenderTheme.h"
#include <pal/spi/cf/CoreTextSPI.h>
#include <wtf/cocoa/TypeCastsCocoa.h>
namespace WebCore {
#if PLATFORM(MAC)
constexpr CGFloat attachmentIconSize = 48;
constexpr CGFloat attachmentIconBackgroundPadding = 6;
constexpr CGFloat attachmentIconBackgroundSize = attachmentIconSize + attachmentIconBackgroundPadding;
constexpr CGFloat attachmentIconSelectionBorderThickness = 1;
constexpr CGFloat attachmentIconBackgroundRadius = 3;
constexpr CGFloat attachmentIconToTitleMargin = 2;
constexpr auto attachmentIconBackgroundColor = Color::black.colorWithAlphaByte(30);
constexpr auto attachmentIconBorderColor = Color::white.colorWithAlphaByte(125);
constexpr CGFloat attachmentTitleFontSize = 12;
constexpr CGFloat attachmentTitleBackgroundRadius = 3;
constexpr CGFloat attachmentTitleBackgroundPadding = 3;
constexpr CGFloat attachmentTitleMaximumWidth = 100 - (attachmentTitleBackgroundPadding * 2);
constexpr CFIndex attachmentTitleMaximumLineCount = 2;
constexpr auto attachmentTitleInactiveBackgroundColor = SRGBA<uint8_t> { 204, 204, 204 };
constexpr auto attachmentTitleInactiveTextColor = SRGBA<uint8_t> { 100, 100, 100 };
constexpr CGFloat attachmentSubtitleFontSize = 10;
constexpr int attachmentSubtitleWidthIncrement = 10;
constexpr auto attachmentSubtitleTextColor = SRGBA<uint8_t> { 82, 145, 214 };
constexpr CGFloat attachmentProgressBarWidth = 30;
constexpr CGFloat attachmentProgressBarHeight = 5;
constexpr CGFloat attachmentProgressBarOffset = -9;
constexpr CGFloat attachmentProgressBarBorderWidth = 1;
constexpr auto attachmentProgressBarBackgroundColor = Color::black.colorWithAlphaByte(89);
constexpr auto attachmentProgressBarFillColor = Color::white;
constexpr auto attachmentProgressBarBorderColor = Color::black.colorWithAlphaByte(128);
constexpr CGFloat attachmentPlaceholderBorderRadius = 5;
constexpr auto attachmentPlaceholderBorderColor = Color::black.colorWithAlphaByte(56);
constexpr CGFloat attachmentPlaceholderBorderWidth = 2;
constexpr CGFloat attachmentPlaceholderBorderDashLength = 6;
constexpr CGFloat attachmentMargin = 3;
static Color titleTextColorForAttachment(const RenderAttachment& attachment, AttachmentLayoutStyle style)
{
Color result = RenderTheme::singleton().systemColor(CSSValueCanvastext, attachment.styleColorOptions());
if (style == AttachmentLayoutStyle::Selected) {
if (attachment.frame().selection().isFocusedAndActive())
result = RenderTheme::singleton().systemColor(CSSValueAppleSystemAlternateSelectedText, attachment.styleColorOptions());
else
result = attachmentTitleInactiveTextColor;
}
return result;
}
void AttachmentLayout::layOutTitle(const RenderAttachment& attachment)
{
CFStringRef language = nullptr; // By not specifying a language we use the system language.
auto font = adoptCF(CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, attachmentTitleFontSize, language));
baseline = CGRound(attachmentIconBackgroundSize + attachmentIconToTitleMargin + CTFontGetAscent(font.get()));
wrappingWidth = attachmentTitleMaximumWidth;
widthPadding = attachmentIconBackgroundSize;
String title = attachment.attachmentElement().attachmentTitleForDisplay();
if (title.isEmpty())
return;
NSDictionary *textAttributes = @{
(__bridge id)kCTFontAttributeName: (__bridge id)font.get(),
(__bridge id)kCTForegroundColorAttributeName: (__bridge id)cachedCGColor(titleTextColorForAttachment(attachment, style)).get()
};
buildWrappedLines(title, font.get(), textAttributes, attachmentTitleMaximumLineCount);
CGFloat yOffset = attachmentIconBackgroundSize + attachmentIconToTitleMargin;
unsigned i = 0;
if (!lines.isEmpty()) {
for (auto& line : lines) {
if (i)
yOffset += origins[i - 1].y - origins[i].y;
line.rect.setY(yOffset);
line.backgroundRect = LayoutRect(line.rect);
line.rect.setY(yOffset - origins.last().y);
line.backgroundRect.inflateX(attachmentTitleBackgroundPadding);
line.backgroundRect = encloseRectToDevicePixels(line.backgroundRect, attachment.document().deviceScaleFactor());
// If the text rects are close in size, the curved enclosing background won't
// look right, so make them the same exact size.
if (i) {
float previousBackgroundRectWidth = lines[i-1].backgroundRect.width();
if (std::abs(line.backgroundRect.width() - previousBackgroundRectWidth) < attachmentTitleBackgroundRadius * 4) {
float newBackgroundRectWidth = std::max(previousBackgroundRectWidth, line.backgroundRect.width());
line.backgroundRect.inflateX((newBackgroundRectWidth - line.backgroundRect.width()) / 2);
lines[i-1].backgroundRect.inflateX((newBackgroundRectWidth - previousBackgroundRectWidth) / 2);
}
}
i++;
}
}
}
void AttachmentLayout::layOutSubtitle(const RenderAttachment& attachment)
{
String subtitleText = attachment.attachmentElement().attachmentSubtitleForDisplay();
if (subtitleText.isEmpty())
return;
auto subtitleColor = attachment.style().colorByApplyingColorFilter(attachmentSubtitleTextColor);
CFStringRef language = nullptr; // By not specifying a language we use the system language.
auto font = adoptCF(CTFontCreateUIFontForLanguage(kCTFontUIFontSystem, attachmentSubtitleFontSize, language));
NSDictionary *textAttributes = @{
(__bridge id)kCTFontAttributeName: (__bridge id)font.get(),
(__bridge id)kCTForegroundColorAttributeName: (__bridge id)cachedCGColor(subtitleColor).get()
};
CGFloat yOffset = 0;
if (!lines.isEmpty())
yOffset = lines.last().backgroundRect.maxY();
else
yOffset = attachmentIconBackgroundSize + attachmentIconToTitleMargin;
buildSingleLine(subtitleText, font.get(), textAttributes);
lines.last().rect.setY(yOffset);
subtitleTextRect = LayoutRect(lines.last().rect);
lines.last().rect.setLocation(subtitleTextRect.minXMaxYCorner());
lines.last().rect.setSize(FloatSize(0, 0));
}
AttachmentLayout::AttachmentLayout(const RenderAttachment& attachment, AttachmentLayoutStyle layoutStyle)
: style(layoutStyle)
{
excludeTypographicLeading = false;
layOutTitle(attachment);
layOutSubtitle(attachment);
iconBackgroundRect = FloatRect(0, 0, attachmentIconBackgroundSize, attachmentIconBackgroundSize);
iconRect = iconBackgroundRect;
iconRect.setSize(FloatSize(attachmentIconSize, attachmentIconSize));
iconRect.move(attachmentIconBackgroundPadding / 2, attachmentIconBackgroundPadding / 2);
attachmentRect = iconBackgroundRect;
for (const auto& line : lines)
attachmentRect.unite(line.backgroundRect);
if (!subtitleTextRect.isEmpty()) {
FloatRect roundedSubtitleTextRect = subtitleTextRect;
roundedSubtitleTextRect.inflateX(attachmentSubtitleWidthIncrement - clampToInteger(ceilf(subtitleTextRect.width())) % attachmentSubtitleWidthIncrement);
attachmentRect.unite(roundedSubtitleTextRect);
}
attachmentRect.inflate(attachmentMargin);
attachmentRect = encloseRectToDevicePixels(attachmentRect, attachment.document().deviceScaleFactor());
}
#endif // PLATFORM(MAC)
#if PLATFORM(IOS_FAMILY)
constexpr CGSize attachmentSize = { 160, 119 };
constexpr CGFloat attachmentBorderRadius = 16;
constexpr auto attachmentBorderColor = SRGBA<uint8_t> { 204, 204, 204 };
static CGFloat attachmentBorderThickness = 1;
constexpr auto attachmentProgressColor = SRGBA<uint8_t> { 222, 222, 222 };
constexpr CGFloat attachmentProgressBorderThickness = 3;
constexpr CGFloat attachmentProgressSize = 36;
constexpr CGFloat attachmentIconSize = 48;
constexpr CGFloat attachmentItemMargin = 8;
constexpr CGFloat attachmentWrappingTextMaximumWidth = 140;
constexpr CFIndex attachmentWrappingTextMaximumLineCount = 2;
static BOOL getAttachmentProgress(const RenderAttachment& attachment, float& progress)
{
auto& progressString = attachment.attachmentElement().attributeWithoutSynchronization(HTMLNames::progressAttr);
if (progressString.isEmpty())
return NO;
bool validProgress;
progress = std::max<float>(std::min<float>(progressString.toFloat(&validProgress), 1), 0);
return validProgress;
}
static RetainPtr<CTFontRef> attachmentActionFont()
{
auto style = kCTUIFontTextStyleFootnote;
auto size = contentSizeCategory();
auto attributes = static_cast<CFDictionaryRef>(@{ (id)kCTFontTraitsAttribute: @{ (id)kCTFontSymbolicTrait: @(kCTFontTraitTightLeading | kCTFontTraitEmphasized) } });
auto emphasizedFontDescriptor = adoptCF(CTFontDescriptorCreateWithTextStyleAndAttributes(style, size, attributes));
return adoptCF(CTFontCreateWithFontDescriptor(emphasizedFontDescriptor.get(), 0, nullptr));
}
static RetainPtr<UIColor> attachmentActionColor(const RenderAttachment& attachment)
{
return cocoaColor(attachment.style().visitedDependentColor(CSSPropertyColor));
}
static RetainPtr<CTFontRef> attachmentTitleFont()
{
auto fontDescriptor = adoptCF(CTFontDescriptorCreateWithTextStyle(kCTUIFontTextStyleShortCaption1, contentSizeCategory(), 0));
return adoptCF(CTFontCreateWithFontDescriptor(fontDescriptor.get(), 0, nullptr));
}
static UIColor *attachmentTitleColor(const RenderAttachment& renderer)
{
return cocoaColor(RenderTheme::singleton().systemColor(CSSValueAppleSystemGray, renderer.styleColorOptions())).autorelease();
}
static RetainPtr<CTFontRef> attachmentSubtitleFont() { return attachmentTitleFont(); }
static UIColor *attachmentSubtitleColor(const RenderAttachment& renderer) { return attachmentTitleColor(renderer); }
static CGFloat shortCaptionPointSizeWithContentSizeCategory(CFStringRef contentSizeCategory)
{
auto descriptor = adoptCF(CTFontDescriptorCreateWithTextStyle(kCTUIFontTextStyleShortCaption1, contentSizeCategory, 0));
auto pointSize = adoptCF(CTFontDescriptorCopyAttribute(descriptor.get(), kCTFontSizeAttribute));
return [dynamic_objc_cast<NSNumber>((__bridge id)pointSize.get()) floatValue];
}
static CGFloat attachmentDynamicTypeScaleFactor()
{
CGFloat fixedPointSize = shortCaptionPointSizeWithContentSizeCategory(kCTFontContentSizeCategoryL);
CGFloat dynamicPointSize = shortCaptionPointSizeWithContentSizeCategory(contentSizeCategory());
if (!dynamicPointSize || !fixedPointSize)
return 1;
return std::max<CGFloat>(1, dynamicPointSize / fixedPointSize);
}
AttachmentLayout::AttachmentLayout(const RenderAttachment& attachment, AttachmentLayoutStyle)
{
excludeTypographicLeading = true;
attachmentRect = FloatRect(0, 0, attachment.width().toFloat(), attachment.height().toFloat());
wrappingWidth = attachmentWrappingTextMaximumWidth * attachmentDynamicTypeScaleFactor();
widthPadding = attachmentRect.width();
hasProgress = getAttachmentProgress(attachment, progress);
String title = attachment.attachmentElement().attachmentTitleForDisplay();
String action = attachment.attachmentElement().attachmentActionForDisplay();
String subtitle = attachment.attachmentElement().attachmentSubtitleForDisplay();
CGFloat yOffset = 0;
if (hasProgress) {
progressRect = FloatRect((attachmentRect.width() / 2) - (attachmentProgressSize / 2), 0, attachmentProgressSize, attachmentProgressSize);
yOffset += attachmentProgressSize + attachmentItemMargin;
}
if (action.isEmpty() && !hasProgress) {
attachment.attachmentElement().requestIconIfNeededWithSize(FloatSize());
FloatSize iconSize = attachment.attachmentElement().iconSize();
icon = attachment.attachmentElement().icon();
if (icon) {
iconRect = FloatRect(FloatPoint((attachmentRect.width() / 2) - (iconSize.width() / 2), 0), iconSize);
yOffset += iconRect.height() + attachmentItemMargin;
}
} else {
NSDictionary *textAttributesTitle = @{
(id)kCTFontAttributeName: (id)attachmentActionFont().get(),
(id)kCTForegroundColorAttributeName: attachmentActionColor(attachment).get()
};
buildWrappedLines(action, attachmentActionFont().get(), textAttributesTitle, attachmentWrappingTextMaximumLineCount);
}
bool forceSingleLineTitle = !action.isEmpty() || !subtitle.isEmpty() || hasProgress;
NSDictionary *textAttributesTitle = @{
(id)kCTFontAttributeName: (id)attachmentTitleFont().get(),
(id)kCTForegroundColorAttributeName: attachmentTitleColor(attachment)
};
buildWrappedLines(title, attachmentTitleFont().get(), textAttributesTitle, forceSingleLineTitle ? 1 : attachmentWrappingTextMaximumLineCount);
NSDictionary *textAttributesSubTitle = @{
(id)kCTFontAttributeName: (id)attachmentSubtitleFont().get(),
(id)kCTForegroundColorAttributeName: attachmentSubtitleColor(attachment)
};
buildSingleLine(subtitle, attachmentSubtitleFont().get(), textAttributesSubTitle);
if (!lines.isEmpty()) {
for (auto& line : lines) {
line.rect.setY(yOffset);
yOffset += line.rect.height() + attachmentItemMargin;
}
}
yOffset -= attachmentItemMargin;
contentYOrigin = (attachmentRect.height() / 2) - (yOffset / 2);
}
#endif // PLATFORM(IOS_FAMILY)
void AttachmentLayout::addLine(CTFontRef font, CTLineRef line, bool isSubtitle = false)
{
CGRect lineBounds = CTLineGetBoundsWithOptions(line, excludeTypographicLeading ? kCTLineBoundsExcludeTypographicLeading : 0);
CGFloat trailingWhitespaceWidth = CTLineGetTrailingWhitespaceWidth(line);
CGFloat lineWidthIgnoringTrailingWhitespace = lineBounds.size.width - trailingWhitespaceWidth;
CGFloat lineHeight = lineBounds.size.height + (excludeTypographicLeading ? lineBounds.origin.y : 0);
lineHeight = isSubtitle ? lineHeight : CGCeiling(lineHeight);
CGFloat xOffset = (widthPadding / 2) - (lineWidthIgnoringTrailingWhitespace / 2);
LabelLine labelLine;
labelLine.font = font;
labelLine.line = line;
labelLine.rect = FloatRect(xOffset, 0, lineWidthIgnoringTrailingWhitespace, lineHeight);
lines.append(labelLine);
}
void AttachmentLayout::buildWrappedLines(String& text, CTFontRef font, NSDictionary *textAttributes, unsigned maximumLineCount)
{
if (text.isEmpty())
return;
RetainPtr attributedText = adoptNS([[NSAttributedString alloc] initWithString:text.createNSString().get() attributes:textAttributes]);
RetainPtr framesetter = adoptCF(CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedText.get()));
CFRange fitRange;
auto textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter.get(), CFRangeMake(0, 0), nullptr, CGSizeMake(wrappingWidth, CGFLOAT_MAX), &fitRange);
auto textPath = adoptCF(CGPathCreateWithRect(CGRectMake(0, 0, textSize.width, textSize.height), nullptr));
auto textFrame = adoptCF(CTFramesetterCreateFrame(framesetter.get(), fitRange, textPath.get(), nullptr));
auto ctLines = CTFrameGetLines(textFrame.get());
auto lineCount = CFArrayGetCount(ctLines);
if (!lineCount)
return;
origins.resize(lineCount);
CTFrameGetLineOrigins(textFrame.get(), CFRangeMake(0, 0), origins.mutableSpan().data());
// Lay out and record the first (maximumLineCount - 1) lines.
CFIndex lineIndex = 0;
auto nonTruncatedLineCount = std::min<CFIndex>(maximumLineCount - 1, lineCount);
for (; lineIndex < nonTruncatedLineCount; ++lineIndex)
addLine(font, (CTLineRef)CFArrayGetValueAtIndex(ctLines, lineIndex));
if (lineIndex == lineCount)
return;
// We had text that didn't fit in the first (maximumLineCount - 1) lines.
// Combine it into one last line, and center-truncate it.
auto firstRemainingLine = (CTLineRef)CFArrayGetValueAtIndex(ctLines, lineIndex);
auto remainingRangeStart = CTLineGetStringRange(firstRemainingLine).location;
auto remainingRange = CFRangeMake(remainingRangeStart, [attributedText length] - remainingRangeStart);
auto remainingPath = adoptCF(CGPathCreateWithRect(CGRectMake(0, 0, CGFLOAT_MAX, CGFLOAT_MAX), nullptr));
auto remainingFrame = adoptCF(CTFramesetterCreateFrame(framesetter.get(), remainingRange, remainingPath.get(), nullptr));
auto ellipsisString = adoptNS([[NSAttributedString alloc] initWithString:@"\u2026" attributes:textAttributes]);
auto ellipsisLine = adoptCF(CTLineCreateWithAttributedString((CFAttributedStringRef)ellipsisString.get()));
auto remainingLine = (CTLineRef)CFArrayGetValueAtIndex(CTFrameGetLines(remainingFrame.get()), 0);
auto truncatedLine = adoptCF(CTLineCreateTruncatedLine(remainingLine, wrappingWidth, kCTLineTruncationMiddle, ellipsisLine.get()));
if (!truncatedLine)
truncatedLine = remainingLine;
addLine(font, truncatedLine.get());
}
void AttachmentLayout::buildSingleLine(const String& text, CTFontRef font, NSDictionary *textAttributes)
{
if (text.isEmpty())
return;
RetainPtr attributedText = adoptNS([[NSAttributedString alloc] initWithString:text.createNSString().get() attributes:textAttributes]);
addLine(font, adoptCF(CTLineCreateWithAttributedString((CFAttributedStringRef)attributedText.get())).get(), true);
}
} // namespace WebCore
#endif // ENABLE(ATTACHMENT_ELEMENT) && PLATFORM(COCOA)