Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.RectF
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
Expand Down Expand Up @@ -119,7 +118,10 @@ class CodeBlockContainerView(
background = null
scaleType = ImageView.ScaleType.CENTER
setImageDrawable(
CopyIconDrawable(secondaryColor(codeBlockStyle.color), ceil(codeBlockStyle.fontSize).toInt()),
CopyIconDrawable(
secondaryColor(codeBlockStyle.color),
ceil(codeBlockStyle.fontSize * HEADER_ICON_SCALE).toInt(),
),
)
setOnClickListener { copyCode() }
}
Expand Down Expand Up @@ -254,16 +256,16 @@ class CodeBlockContainerView(
private val paint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = size / 12f
strokeWidth = size / 20f
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
this.color = color
}

private val glyph = CopyGlyph()

override fun draw(canvas: Canvas) {
val u = bounds.width() / 24f
canvas.drawRoundRect(RectF(8 * u, 2 * u, 22 * u, 16 * u), 2 * u, 2 * u, paint)
canvas.drawRoundRect(RectF(2 * u, 8 * u, 16 * u, 22 * u), 2 * u, 2 * u, paint)
glyph.draw(canvas, bounds.width().toFloat(), paint)
}

override fun getIntrinsicWidth() = size
Expand All @@ -285,6 +287,12 @@ class CodeBlockContainerView(
companion object {
private const val HEADER_LABEL_SCALE = 0.85f

// Drawable box for the copy glyph, as a multiple of the code font size.
// iOS sizes the SF Symbol by point size (kENRMHeaderIconScale = 0.72) but
// the symbol renders roughly half again as tall as its point size; this box
// is scaled up to land the drawn glyph at the same on-screen size.
private const val HEADER_ICON_SCALE = 1.08f

private val headerTypeface: Typeface = Typeface.create("sans-serif-medium", Typeface.NORMAL)

private fun secondaryColor(color: Int): Int = (color and 0x00FFFFFF) or (0x99 shl 24)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,19 @@ object ContextMenuPopup {
color = Color.parseColor("#333333")
}

private val copyGlyph = CopyGlyph()

// COPY mirrors SF Symbol doc.on.doc, DOCUMENT mirrors doc.text; the copy
// glyph is shared with the header button in CodeBlockContainerView.
override fun draw(canvas: Canvas) {
val u = bounds.width() / 24f
if (icon == Icon.COPY) {
canvas.drawRoundRect(RectF(8 * u, 2 * u, 22 * u, 16 * u), 2 * u, 2 * u, paint)
canvas.drawRoundRect(RectF(2 * u, 8 * u, 16 * u, 22 * u), 2 * u, 2 * u, paint)
copyGlyph.draw(canvas, bounds.width().toFloat(), paint)
} else {
canvas.drawRoundRect(RectF(4 * u, 2 * u, 20 * u, 22 * u), 2 * u, 2 * u, paint)
canvas.drawLine(8 * u, 8 * u, 16 * u, 8 * u, paint)
canvas.drawLine(8 * u, 12 * u, 16 * u, 12 * u, paint)
canvas.drawLine(8 * u, 16 * u, 13 * u, 16 * u, paint)
canvas.drawRoundRect(RectF(5 * u, 3 * u, 19 * u, 21 * u), 2.5f * u, 2.5f * u, paint)
canvas.drawLine(8.5f * u, 9 * u, 15.5f * u, 9 * u, paint)
canvas.drawLine(8.5f * u, 12.5f * u, 15.5f * u, 12.5f * u, paint)
canvas.drawLine(8.5f * u, 16 * u, 13 * u, 16 * u, paint)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.swmansion.enriched.markdown.views

import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode

/**
* Draws the copy glyph so Android matches the SF Symbol doc.on.doc rendered on
* iOS: two portrait document pages with a folded (dog-eared) top-right corner,
* a front page in the lower-left laid over a back page in the upper-right.
*
* The back page is punched out where the front page (plus a hairline of
* clearance) covers it, so the front reads as sitting on top exactly as the SF
* Symbol does, instead of both outlines crossing. The clear is done inside an
* offscreen layer so the punched region reveals whatever background the drawable
* sits on, without the glyph needing to know that color.
*
* One instance is held per drawable rather than shared: the paths and clear
* paint are reused across draws to keep draw() allocation-free, and two drawables
* of different sizes must not share those buffers. All coordinates use a 24-unit
* grid scaled to the drawable width, matching the grid the iOS symbol is authored
* on.
*/
internal class CopyGlyph {
private val back = Path()
private val front = Path()
private val backFold = Path()
private val frontFold = Path()

// Color and colorFilter are irrelevant under PorterDuff.CLEAR; only the
// widened stroke (set per draw) matters, so this is configured once.
private val clearPaint =
Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL_AND_STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
}

fun draw(
canvas: Canvas,
width: Float,
stroke: Paint,
) {
val u = width / 24f
val fold = 4f * u
val corner = 2f * u

buildPage(back, 9 * u, 1 * u, 20 * u, 17 * u, fold, corner)
buildPage(front, 3 * u, 7 * u, 14 * u, 23 * u, fold, corner)
buildFold(backFold, 20 * u, 1 * u, fold)
buildFold(frontFold, 14 * u, 7 * u, fold)
clearPaint.strokeWidth = stroke.strokeWidth * 3f

val saved = canvas.saveLayer(0f, 0f, width, width, null)
canvas.drawPath(back, stroke)
canvas.drawPath(backFold, stroke)
canvas.drawPath(front, clearPaint)
canvas.drawPath(front, stroke)
canvas.drawPath(frontFold, stroke)
canvas.restoreToCount(saved)
}

// A page outline whose top-right corner is cut back by fold, with the other
// three corners rounded by corner.
private fun buildPage(
path: Path,
l: Float,
t: Float,
r: Float,
b: Float,
fold: Float,
corner: Float,
) {
path.rewind()
path.moveTo(l, t + corner)
path.quadTo(l, t, l + corner, t)
path.lineTo(r - fold, t)
path.lineTo(r, t + fold)
path.lineTo(r, b - corner)
path.quadTo(r, b, r - corner, b)
path.lineTo(l + corner, b)
path.quadTo(l, b, l, b - corner)
path.close()
}

// The two inner edges of the folded-over corner, at the page's top-right.
private fun buildFold(
path: Path,
r: Float,
t: Float,
fold: Float,
) {
path.rewind()
path.moveTo(r - fold, t)
path.lineTo(r - fold, t + fold)
path.lineTo(r, t + fold)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
// lines never wrap, the height is independent of the available width.

static const CGFloat kENRMHeaderLabelScale = 0.85;
// The copy glyph reads cleaner a touch smaller than the language label and at a
// regular (not medium) weight; sized independently so the two never couple.
static const CGFloat kENRMHeaderIconScale = 0.72;
static const CGFloat kENRMHeaderSecondaryAlpha = 0.6;
static const CGFloat kENRMHeaderDividerAlpha = 0.2;

Expand Down Expand Up @@ -193,15 +196,19 @@ - (instancetype)initWithConfig:(StyleConfig *)config

_copyButton = [UIButton buttonWithType:UIButtonTypeSystem];
UIImageSymbolConfiguration *symbolConfig =
[UIImageSymbolConfiguration configurationWithPointSize:[config codeBlockFont].pointSize * kENRMHeaderLabelScale
weight:UIImageSymbolWeightMedium];
[UIImageSymbolConfiguration configurationWithPointSize:[config codeBlockFont].pointSize * kENRMHeaderIconScale
weight:UIImageSymbolWeightRegular];
[_copyButton setImage:[UIImage systemImageNamed:@"doc.on.doc" withConfiguration:symbolConfig]
forState:UIControlStateNormal];
_copyButton.tintColor = [[config codeBlockColor] colorWithAlphaComponent:kENRMHeaderSecondaryAlpha];
[_copyButton addTarget:self action:@selector(copyCodeToPasteboard) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_copyButton];
#else
NSImage *copyImage = [NSImage imageWithSystemSymbolName:@"doc.on.doc" accessibilityDescription:nil];
NSImageSymbolConfiguration *symbolConfig =
[NSImageSymbolConfiguration configurationWithPointSize:[config codeBlockFont].pointSize * kENRMHeaderIconScale
weight:NSFontWeightRegular];
NSImage *copyImage = [[NSImage imageWithSystemSymbolName:@"doc.on.doc"
accessibilityDescription:nil] imageWithSymbolConfiguration:symbolConfig];
_copyButton = [NSButton buttonWithImage:copyImage target:self action:@selector(copyCodeToPasteboard)];
_copyButton.bordered = NO;
_copyButton.contentTintColor = [[config codeBlockColor] colorWithAlphaComponent:kENRMHeaderSecondaryAlpha];
Expand Down
Loading