diff --git a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Icons.kt b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Icons.kt index 90143bf843..5b734eb1c0 100644 --- a/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Icons.kt +++ b/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/components/Icons.kt @@ -15,6 +15,8 @@ import androidx.compose.ui.unit.dp import org.jetbrains.jewel.samples.standalone.StandaloneSampleIcons import org.jetbrains.jewel.ui.component.GroupHeader import org.jetbrains.jewel.ui.component.Icon +import org.jetbrains.jewel.ui.painter.badge.DotBadgeShape +import org.jetbrains.jewel.ui.painter.hints.Badge import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider @Composable @@ -38,5 +40,8 @@ internal fun Icons() { ColorFilter.tint(Color.Magenta, BlendMode.Multiply), Modifier.size(128.dp), ) + + val badged by iconProvider.getPainter(Badge(Color.Red, DotBadgeShape.Default)) + Icon(badged, "Jewel Logo", Modifier.size(20.dp)) } } diff --git a/ui/api/ui.api b/ui/api/ui.api index 4ebddb7e4a..1e6b5df87b 100644 --- a/ui/api/ui.api +++ b/ui/api/ui.api @@ -2010,6 +2010,11 @@ public final class org/jetbrains/jewel/ui/component/styling/TooltipStylingKt { public static final fun getLocalTooltipStyle ()Landroidx/compose/runtime/ProvidableCompositionLocal; } +public final class org/jetbrains/jewel/ui/painter/BadgePainter : org/jetbrains/jewel/ui/painter/DelegatePainter { + public static final field $stable I + public synthetic fun (Landroidx/compose/ui/graphics/painter/Painter;JLorg/jetbrains/jewel/ui/painter/badge/BadgeShape;Lkotlin/jvm/internal/DefaultConstructorMarker;)V +} + public abstract class org/jetbrains/jewel/ui/painter/BasePainterHintsProvider : org/jetbrains/jewel/ui/painter/PainterHintsProvider { public static final field $stable I public fun (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;)V @@ -2032,6 +2037,23 @@ public final class org/jetbrains/jewel/ui/painter/CommonPainterHintsProvider : o public fun priorityHints (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)Ljava/util/List; } +public class org/jetbrains/jewel/ui/painter/DelegatePainter : androidx/compose/ui/graphics/painter/Painter { + public static final field $stable I + public fun (Landroidx/compose/ui/graphics/painter/Painter;)V + protected fun applyAlpha (F)Z + protected fun applyColorFilter (Landroidx/compose/ui/graphics/ColorFilter;)Z + protected fun applyLayoutDirection (Landroidx/compose/ui/unit/LayoutDirection;)Z + protected final fun drawDelegate (Landroidx/compose/ui/graphics/drawscope/DrawScope;)V + protected final fun getAlpha ()F + protected final fun getFilter ()Landroidx/compose/ui/graphics/ColorFilter; + public fun getIntrinsicSize-NH-jbRc ()J + protected final fun getLayoutDirection ()Landroidx/compose/ui/unit/LayoutDirection; + protected fun onDraw (Landroidx/compose/ui/graphics/drawscope/DrawScope;)V + protected final fun setAlpha (F)V + protected final fun setFilter (Landroidx/compose/ui/graphics/ColorFilter;)V + protected final fun setLayoutDirection (Landroidx/compose/ui/unit/LayoutDirection;)V +} + public abstract interface class org/jetbrains/jewel/ui/painter/PainterHint { public static final field None Lorg/jetbrains/jewel/ui/painter/PainterHint$None; public abstract fun canApplyTo (Ljava/lang/String;)Z @@ -2103,6 +2125,14 @@ public final class org/jetbrains/jewel/ui/painter/PainterSvgPatchHint$DefaultImp public static fun canApplyTo (Lorg/jetbrains/jewel/ui/painter/PainterSvgPatchHint;Ljava/lang/String;)Z } +public abstract interface class org/jetbrains/jewel/ui/painter/PainterWrapperHint : org/jetbrains/jewel/ui/painter/PainterHint { + public abstract fun wrap (Landroidx/compose/ui/graphics/painter/Painter;)Landroidx/compose/ui/graphics/painter/Painter; +} + +public final class org/jetbrains/jewel/ui/painter/PainterWrapperHint$DefaultImpls { + public static fun canApplyTo (Lorg/jetbrains/jewel/ui/painter/PainterWrapperHint;Ljava/lang/String;)Z +} + public final class org/jetbrains/jewel/ui/painter/ResourcePainterProvider : org/jetbrains/jewel/ui/painter/PainterProvider { public static final field $stable I public fun (Ljava/lang/String;[Ljava/lang/ClassLoader;)V @@ -2129,6 +2159,35 @@ public final class org/jetbrains/jewel/ui/painter/XmlPainterHint$DefaultImpls { public static fun canApplyTo (Lorg/jetbrains/jewel/ui/painter/XmlPainterHint;Ljava/lang/String;)Z } +public abstract interface class org/jetbrains/jewel/ui/painter/badge/BadgeShape : androidx/compose/ui/graphics/Shape { + public abstract fun createHoleOutline-Pq9zytI (JLandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/unit/Density;)Landroidx/compose/ui/graphics/Outline; +} + +public final class org/jetbrains/jewel/ui/painter/badge/DotBadgeShape : org/jetbrains/jewel/ui/painter/badge/BadgeShape { + public static final field $stable I + public static final field Companion Lorg/jetbrains/jewel/ui/painter/badge/DotBadgeShape$Companion; + public fun ()V + public fun (FFFF)V + public synthetic fun (FFFFILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun createHoleOutline-Pq9zytI (JLandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/unit/Density;)Landroidx/compose/ui/graphics/Outline; + public fun createOutline-Pq9zytI (JLandroidx/compose/ui/unit/LayoutDirection;Landroidx/compose/ui/unit/Density;)Landroidx/compose/ui/graphics/Outline; + public fun equals (Ljava/lang/Object;)Z + public final fun getBorder ()F + public final fun getRadius ()F + public final fun getX ()F + public final fun getY ()F + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class org/jetbrains/jewel/ui/painter/badge/DotBadgeShape$Companion { + public final fun getDefault ()Lorg/jetbrains/jewel/ui/painter/badge/DotBadgeShape; +} + +public final class org/jetbrains/jewel/ui/painter/hints/BadgeKt { + public static final fun Badge-DxMtmZc (JLorg/jetbrains/jewel/ui/painter/badge/BadgeShape;)Lorg/jetbrains/jewel/ui/painter/PainterHint; +} + public final class org/jetbrains/jewel/ui/painter/hints/DarkKt { public static final fun Dark (Z)Lorg/jetbrains/jewel/ui/painter/PainterHint; public static synthetic fun Dark$default (ZILjava/lang/Object;)Lorg/jetbrains/jewel/ui/painter/PainterHint; diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BadgePainter.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BadgePainter.kt new file mode 100644 index 0000000000..7ca2ea2441 --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BadgePainter.kt @@ -0,0 +1,60 @@ +package org.jetbrains.jewel.ui.painter + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.withSaveLayer +import androidx.compose.ui.unit.Density +import org.jetbrains.jewel.ui.painter.badge.BadgeShape + +class BadgePainter(source: Painter, private val color: Color, private val shape: BadgeShape) : DelegatePainter(source) { + + /** + * Optional [Paint] used to draw contents into an offscreen layer in order to apply + * alpha or [ColorFilter] parameters accordingly. If no alpha or [ColorFilter] is + * provided or the [Painter] implementation implements [applyAlpha] and + * [applyColorFilter] then this paint is not used + */ + private var layerPaint: Paint? = null + + /** + * Lazily create a [Paint] object or return the existing instance if it is already allocated + */ + private fun obtainPaint(): Paint { + var target = layerPaint + if (target == null) { + target = Paint() + layerPaint = target + } + return target + } + + private fun DrawScope.drawHole() { + val badge = shape.createHoleOutline(size, layoutDirection, Density(density)) + drawOutline(badge, Color.White, alpha, blendMode = BlendMode.Clear) + } + + private fun DrawScope.drawBadge() { + val badge = shape.createOutline(size, layoutDirection, Density(density)) + drawOutline(badge, color, alpha) + } + + override fun DrawScope.onDraw() { + val layerRect = Rect(Offset.Zero, Size(size.width, size.height)) + drawIntoCanvas { canvas -> + canvas.withSaveLayer(layerRect, obtainPaint()) { + drawDelegate() + drawHole() + drawBadge() + } + } + } +} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/DelegatePainter.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/DelegatePainter.kt new file mode 100644 index 0000000000..9eaff88436 --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/DelegatePainter.kt @@ -0,0 +1,44 @@ +package org.jetbrains.jewel.ui.painter + +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.LayoutDirection + +open class DelegatePainter(private val delegate: Painter) : Painter() { + + override val intrinsicSize: Size + get() = delegate.intrinsicSize + + protected var alpha: Float = 1f + + protected var filter: ColorFilter? = null + + protected var layoutDirection: LayoutDirection = LayoutDirection.Ltr + + override fun applyAlpha(alpha: Float): Boolean { + this.alpha = alpha + return true + } + + override fun applyColorFilter(colorFilter: ColorFilter?): Boolean { + this.filter = colorFilter + return true + } + + override fun applyLayoutDirection(layoutDirection: LayoutDirection): Boolean { + this.layoutDirection = layoutDirection + return true + } + + protected fun DrawScope.drawDelegate() { + with(delegate) { + draw(this@drawDelegate.size, alpha, filter) + } + } + + override fun DrawScope.onDraw() { + drawDelegate() + } +} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PainterHint.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PainterHint.kt index 27f6cec471..5b56cca7a5 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PainterHint.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PainterHint.kt @@ -1,6 +1,7 @@ package org.jetbrains.jewel.ui.painter import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.painter.Painter import org.w3c.dom.Element /** @@ -102,6 +103,12 @@ interface PainterSvgPatchHint : SvgPainterHint { fun patch(element: Element) } +@Immutable +interface PainterWrapperHint : PainterHint { + + fun wrap(painter: Painter): Painter +} + /** * A [PainterHint] that adds a prefix to a resource file name, without * changing the rest of the path. diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt index d39c36f4c3..93fc79cb6d 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt @@ -113,11 +113,18 @@ class ResourcePainterProvider( val extension = basePath.substringAfterLast(".").lowercase() - return when (extension) { + var painter = when (extension) { "svg" -> createSvgPainter(url, density, hints) "xml" -> createVectorDrawablePainter(url, density) else -> createBitmapPainter(url, density) } + + for (hint in hints) { + if (hint !is PainterWrapperHint) continue + painter = hint.wrap(painter) + } + + return painter } private fun resolveResource(path: String): URL? { diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/BadgeShape.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/BadgeShape.kt new file mode 100644 index 0000000000..081beea77b --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/BadgeShape.kt @@ -0,0 +1,15 @@ +package org.jetbrains.jewel.ui.painter.badge + +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +interface BadgeShape : Shape { + + fun createHoleOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline +} + +internal val emptyOutline = Outline.Rectangle(Rect.Zero) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/DotBadgeShape.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/DotBadgeShape.kt new file mode 100644 index 0000000000..ad68f4ad3c --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/DotBadgeShape.kt @@ -0,0 +1,64 @@ +package org.jetbrains.jewel.ui.painter.badge + +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import org.jetbrains.jewel.foundation.GenerateDataFunctions + +/** + * @see com.intellij.ui.BadgeDotProvider + */ +@GenerateDataFunctions +class DotBadgeShape( + val x: Float = 16.5f / 20, + val y: Float = 3.5f / 20, + val radius: Float = 3.5f / 20, + val border: Float = 1.5f / 20, +) : BadgeShape { + + override fun createHoleOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline = + createOutline(size, hole = true) + + override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline = + createOutline(size, hole = false) + + private fun createOutline(size: Size, hole: Boolean): Outline { + val dotSize = size.width.coerceAtMost(size.height) + + if (dotSize <= 0) return emptyOutline + + val radius = dotSize * radius + if (radius <= 0) return emptyOutline + + val x = size.width * x + if (0 > x + radius || x - radius > size.width) return emptyOutline + + val y = size.height * y + if (0 > y + radius || y - radius > size.height) return emptyOutline + + val border = when { + hole -> dotSize * border + else -> 0.0f + } + + val r = radius + border.coerceAtLeast(0.0f) + + return Outline.Rounded( + RoundRect( + left = x - r, + top = y - r, + right = x + r, + bottom = y + r, + cornerRadius = CornerRadius(r), + ), + ) + } + + companion object { + + val Default = DotBadgeShape() + } +} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Badge.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Badge.kt new file mode 100644 index 0000000000..ae06d28bbf --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Badge.kt @@ -0,0 +1,38 @@ +package org.jetbrains.jewel.ui.painter.hints + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.isSpecified +import androidx.compose.ui.graphics.painter.Painter +import org.jetbrains.jewel.ui.painter.BadgePainter +import org.jetbrains.jewel.ui.painter.PainterHint +import org.jetbrains.jewel.ui.painter.PainterWrapperHint +import org.jetbrains.jewel.ui.painter.badge.BadgeShape + +private class BadgeImpl(private val color: Color, private val shape: BadgeShape) : PainterWrapperHint { + + override fun wrap(painter: Painter): Painter = BadgePainter(painter, color, shape) + + override fun toString(): String = "Badge(color=$color, shape=$shape)" + + override fun hashCode(): Int { + var result = color.hashCode() + result = 31 * result + shape.hashCode() + return result + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BadgeImpl) return false + + if (color != other.color) return false + if (shape != other.shape) return false + + return true + } +} + +fun Badge(color: Color, shape: BadgeShape): PainterHint = if (color.isSpecified) { + BadgeImpl(color, shape) +} else { + PainterHint.None +}