Skip to content

Commit

Permalink
Support .redacted(reason: .placeholder)
Browse files Browse the repository at this point in the history
  • Loading branch information
aabewhite committed Sep 13, 2024
1 parent 1685b6b commit 90deb26
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 49 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,17 @@ Support levels:
</details>
</td>
</tr>
<tr>
<td>🟡</td>
<td>
<details>
<summary><code>.redacted</code></summary>
<ul>
<li>Only <code>RedactionReasons.placeholder</code> is supported</li>
</ul>
</details>
</td>
</tr>
<tr>
<td>✅</td>
<td><code>.refreshable</code></td>
Expand Down
10 changes: 6 additions & 4 deletions Sources/SkipUI/SkipUI/Color/Color.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,12 @@ public struct Color: ShapeStyle, Hashable, Sendable {
})

/// Use for placeholder content.
static let placeholder = Color(colorImpl: {
// Close to iOS's AsyncImage placeholder values
ComposeColor(light: 0xFFDDDDDD, dark: 0xFF777777)
})
static let placeholderOpacity = 0.2

/// Use for placeholder content.
static var placeholder: Color {
_primary.opacity(placeholderOpacity)
}

fileprivate static let _primary = Color(colorImpl: {
MaterialTheme.colorScheme.onBackground
Expand Down
57 changes: 37 additions & 20 deletions Sources/SkipUI/SkipUI/Components/Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathFillType
import androidx.compose.ui.graphics.SolidColor
Expand All @@ -34,6 +36,7 @@ import androidx.compose.ui.graphics.asAndroidPath
import androidx.compose.ui.graphics.asComposePath
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.PathBuilder
Expand Down Expand Up @@ -137,19 +140,11 @@ public struct Image : View, Equatable {
.diskCacheKey(url.description)
.build()


let tintColor = EnvironmentValues.shared._foregroundStyle?.asColor(opacity: 1.0, animationContext: context) ?? Color.primary.colorImpl()
let colorFilter: ColorFilter?
if let tintColor, asset.isTemplateImage == true {
colorFilter = ColorFilter.tint(tintColor)
} else {
colorFilter = nil
}

let tintColor = asset.isTemplateImage ? EnvironmentValues.shared._foregroundStyle?.asColor(opacity: 1.0, animationContext: context) ?? Color.primary.colorImpl() : nil
SubcomposeAsyncImage(model: model, contentDescription: nil, loading: { _ in
}, success: { state in
let aspect = EnvironmentValues.shared._aspectRatio
ComposePainter(painter: self.painter, colorFilter: colorFilter, scale: scale, aspectRatio: aspect?.0, contentMode: aspect?.1)
ComposePainter(painter: self.painter, tintColor: tintColor, scale: scale, aspectRatio: aspect?.0, contentMode: aspect?.1)
}, error: { state in
})
}
Expand Down Expand Up @@ -313,7 +308,16 @@ public struct Image : View, Equatable {
ComposePainter(painter: painter, scale: scale, aspectRatio: aspectRatio, contentMode: contentMode)
}

@Composable private func ComposePainter(painter: Painter, scale: CGFloat = 1.0, colorFilter: ColorFilter? = nil, aspectRatio: Double?, contentMode: ContentMode?) {
@Composable private func ComposePainter(painter: Painter, scale: CGFloat = 1.0, tintColor: androidx.compose.ui.graphics.Color? = nil, aspectRatio: Double?, contentMode: ContentMode?) {
let isPlaceholder = EnvironmentValues.shared.redactionReasons.contains(.placeholder)
let colorFilter: ColorFilter?
if let tintColor {
colorFilter = isPlaceholder ? placeholderColorFilter(color: tintColor.copy(alpha: Float(Color.placeholderOpacity))) : ColorFilter.tint(tintColor)
} else if isPlaceholder {
colorFilter = placeholderColorFilter(color: Color.placeholder.colorImpl())
} else {
colorFilter = nil
}
switch resizingMode {
case .stretch:
let scale = contentScale(aspectRatio: aspectRatio, contentMode: contentMode)
Expand Down Expand Up @@ -349,16 +353,10 @@ public struct Image : View, Equatable {
switch resizingMode {
case .stretch:
let painter = rememberVectorPainter(image)
let colorFilter: ColorFilter?
if let tintColor {
colorFilter = ColorFilter.tint(tintColor)
} else {
colorFilter = nil
}
ComposePainter(painter: painter, colorFilter: colorFilter, aspectRatio: aspectRatio, contentMode: contentMode)
ComposePainter(painter: painter, tintColor: tintColor, aspectRatio: aspectRatio, contentMode: contentMode)
default: // TODO: .tile
let textStyle = EnvironmentValues.shared.font?.fontImpl() ?? LocalTextStyle.current
let modifier: Modifier
var modifier: Modifier
if textStyle.fontSize.isSp {
let textSizeDp = with(LocalDensity.current) {
textStyle.fontSize.toDp()
Expand All @@ -368,8 +366,27 @@ public struct Image : View, Equatable {
} else {
modifier = Modifier
}
Icon(imageVector: image, contentDescription: name, modifier: modifier, tint: tintColor ?? androidx.compose.ui.graphics.Color.Unspecified)
let isPlaceholder = EnvironmentValues.shared.redactionReasons.contains(RedactionReasons.placeholder)
if isPlaceholder {
modifier = modifier.paint(ColorPainter(tintColor.copy(alpha: Float(Color.placeholderOpacity))))
}
Icon(imageVector: image, contentDescription: name, modifier: modifier, tint: isPlaceholder ? androidx.compose.ui.graphics.Color.Transparent : tintColor)
}
}

private func placeholderColorFilter(color: androidx.compose.ui.graphics.Color) -> ColorFilter {
let matrix = ColorMatrix().apply {
set(0, 0, Float(0)) // Do not preserve original R
set(1, 1, Float(0)) // Do not preserve original G
set(2, 2, Float(0)) // Do not preserve original B
set(3, 3, Float(0)) // Do not preserve original A

set(0, 4, color.red * 255) // Use given color's R
set(1, 4, color.green * 255) // Use given color's G
set(2, 4, color.blue * 255) // Use given color's B
set(3, 4, color.alpha * 255) // Use given color's A
}
return ColorFilter.colorMatrix(matrix)
}

private func contentScale(aspectRatio: Double?, contentMode: ContentMode?) -> ContentScale {
Expand Down
12 changes: 0 additions & 12 deletions Sources/SkipUI/SkipUI/Text/Font.swift
Original file line number Diff line number Diff line change
Expand Up @@ -386,18 +386,6 @@ public enum LegibilityWeight : Hashable, Sendable {
case bold
}

public struct RedactionReasons : OptionSet, Sendable {
public let rawValue: Int

public init(rawValue: Int) {
self.rawValue = rawValue
}

public static let placeholder = RedactionReasons(rawValue: 1 << 0)
public static let privacy = RedactionReasons(rawValue: 1 << 1)
public static let invalidated = RedactionReasons(rawValue: 1 << 2)
}

#if !SKIP

// Unneeded stubs:
Expand Down
56 changes: 43 additions & 13 deletions Sources/SkipUI/SkipUI/Text/Text.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,12 +307,15 @@ struct _Text: View, Equatable {
font = font.italic()
}
let textDecoration = textEnvironment.textDecoration
let redaction = EnvironmentValues.shared.redactionReasons

var textColor: androidx.compose.ui.graphics.Color? = nil
var textBrush: Brush? = nil
if let foregroundStyle = EnvironmentValues.shared._foregroundStyle {
if let color = foregroundStyle.asColor(opacity: 1.0, animationContext: context) {
textColor = color
} else if !redaction.isEmpty {
textColor = Color.primary.colorImpl()
} else {
textBrush = foregroundStyle.asBrush(opacity: 1.0, animationContext: context)
}
Expand All @@ -323,6 +326,7 @@ struct _Text: View, Equatable {
} else {
textColor = EnvironmentValues.shared._placement.contains(ViewPlacement.systemTextColor) ? androidx.compose.ui.graphics.Color.Unspecified : Color.primary.colorImpl()
}

let textAlign = EnvironmentValues.shared.multilineTextAlignment.asTextAlign()
let maxLines = max(1, EnvironmentValues.shared.lineLimit ?? Int.MAX_VALUE)
var style = font.fontImpl()
Expand All @@ -332,11 +336,22 @@ struct _Text: View, Equatable {
if let textBrush {
style = style.copy(brush: textBrush)
}
if redaction.contains(RedactionReasons.placeholder) {
if let textColor {
style = style.copy(background: textColor.copy(alpha: textColor.alpha * Float(Color.placeholderOpacity)))
}
textColor = androidx.compose.ui.graphics.Color.Transparent
}

let animatable = style.asAnimatable(context: context)
if let locnode {
let layoutResult = remember { mutableStateOf<TextLayoutResult?>(nil) }
let linkColor = EnvironmentValues.shared._tint?.colorImpl() ?? Color.accentColor.colorImpl()
let annotatedText = annotatedString(markdown: locnode, interpolations: interpolations, linkColor: linkColor, isUppercased: isUppercased, isLowercased: isLowercased)
let isPlaceholder = redaction.contains(RedactionReasons.placeholder)
var linkColor = EnvironmentValues.shared._tint?.colorImpl() ?? Color.accentColor.colorImpl()
if isPlaceholder {
linkColor = linkColor.copy(alpha: linkColor.alpha * Float(Color.placeholderOpacity))
}
let annotatedText = annotatedString(markdown: locnode, interpolations: interpolations, linkColor: linkColor, isUppercased: isUppercased, isLowercased: isLowercased, isRedacted: isPlaceholder)
let links = annotatedText.getUrlAnnotations(start: 0, end: annotatedText.length)
var modifier = context.modifier
if !links.isEmpty() {
Expand Down Expand Up @@ -368,16 +383,16 @@ struct _Text: View, Equatable {
}
}

private func annotatedString(markdown: MarkdownNode, interpolations: kotlin.collections.List<AnyHashable>?, linkColor: androidx.compose.ui.graphics.Color, isUppercased: Bool, isLowercased: Bool) -> AnnotatedString {
private func annotatedString(markdown: MarkdownNode, interpolations: kotlin.collections.List<AnyHashable>?, linkColor: androidx.compose.ui.graphics.Color, isUppercased: Bool, isLowercased: Bool, isRedacted: Bool) -> AnnotatedString {
return buildAnnotatedString {
append(markdown: markdown, to: self, interpolations: interpolations, linkColor: linkColor, isUppercased: isUppercased, isLowercased: isLowercased)
append(markdown: markdown, to: self, interpolations: interpolations, linkColor: linkColor, isUppercased: isUppercased, isLowercased: isLowercased, isRedacted: isRedacted)
}
}

// SKIP INSERT: @OptIn(ExperimentalTextApi::class)
private func append(markdown: MarkdownNode, to builder: AnnotatedString.Builder, interpolations: kotlin.collections.List<AnyHashable>?, isFirstChild: Bool = true, linkColor: androidx.compose.ui.graphics.Color, isUppercased: Bool, isLowercased: Bool) {
private func append(markdown: MarkdownNode, to builder: AnnotatedString.Builder, interpolations: kotlin.collections.List<AnyHashable>?, isFirstChild: Bool = true, linkColor: androidx.compose.ui.graphics.Color, isUppercased: Bool, isLowercased: Bool, isRedacted: Bool) {
func appendChildren() {
markdown.children?.forEachIndexed { append(markdown: $1, to: builder, interpolations: interpolations, isFirstChild: $0 == 0, linkColor: linkColor, isUppercased: isUppercased, isLowercased: isLowercased) }
markdown.children?.forEachIndexed { append(markdown: $1, to: builder, interpolations: interpolations, isFirstChild: $0 == 0, linkColor: linkColor, isUppercased: isUppercased, isLowercased: isLowercased, isRedacted: isRedacted) }
}

switch markdown.type {
Expand All @@ -402,7 +417,11 @@ struct _Text: View, Equatable {
appendChildren()
builder.pop()
case MarkdownNode.NodeType.link:
builder.pushStyle(SpanStyle(color: linkColor))
if isRedacted {
builder.pushStyle(SpanStyle(background: linkColor))
} else {
builder.pushStyle(SpanStyle(color: linkColor))
}
builder.pushUrlAnnotation(UrlAnnotation(markdown.formattedString(interpolations) ?? ""))
appendChildren()
builder.pop()
Expand Down Expand Up @@ -615,12 +634,7 @@ extension View {

public func redacted(reason: RedactionReasons) -> some View {
#if SKIP
return ComposeModifierView(contentView: self) { view, context in
// See Redacted.kt
Redacted(context: context, color: Color.placeholder.colorImpl()) { context in
view.Compose(context: context)
}
}
return environment(\.redactionReasons, reason)
#else
return self
#endif
Expand Down Expand Up @@ -696,6 +710,22 @@ extension View {
}
}

public struct RedactionReasons : OptionSet, Sendable {
public let rawValue: Int

public init(rawValue: Int) {
self.rawValue = rawValue
}

public static let placeholder = RedactionReasons(rawValue: 1 << 0)

@available(*, unavailable)
public static let privacy = RedactionReasons(rawValue: 1 << 1)

@available(*, unavailable)
public static let invalidated = RedactionReasons(rawValue: 1 << 2)
}

#if false

// TODO: Process for use in SkipUI
Expand Down

0 comments on commit 90deb26

Please sign in to comment.