| 1 |
package io.oxygenhub.android.widget |
| 2 |
|
| 3 |
import android.graphics.Bitmap |
| 4 |
import android.graphics.Canvas |
| 5 |
import android.graphics.LinearGradient |
| 6 |
import android.graphics.Paint |
| 7 |
import android.graphics.Path |
| 8 |
import android.graphics.Shader |
| 9 |
import androidx.compose.ui.graphics.Color |
| 10 |
import androidx.compose.ui.graphics.toArgb |
| 11 |
import io.oxygenhub.android.api.UserSky |
| 12 |
import io.oxygenhub.android.ui.backdrop.Frame |
| 13 |
import io.oxygenhub.android.ui.backdrop.STAR |
| 14 |
import io.oxygenhub.android.ui.backdrop.skyEvents |
| 15 |
import io.oxygenhub.android.ui.backdrop.straight |
| 16 |
import io.oxygenhub.android.ui.theme.DarkBad |
| 17 |
import io.oxygenhub.android.ui.theme.DarkOk |
| 18 |
import io.oxygenhub.android.ui.theme.DarkWarn |
| 19 |
import io.oxygenhub.android.ui.theme.LightBad |
| 20 |
import io.oxygenhub.android.ui.theme.LightOk |
| 21 |
import io.oxygenhub.android.ui.theme.LightWarn |
| 22 |
import uniffi.rust_core.Backdrop as Scene |
| 23 |
|
| 24 |
/** |
| 25 |
* Your sky behind the widget: the dashboard's scene, stepped in rust-core |
| 26 |
* until it settles and drawn once into a bitmap at the widget's size. A |
| 27 |
* widget cannot animate, so each refresh draws it anew from the caller's |
| 28 |
* own activity; it is real data, never decoration (the hub's |
| 29 |
* docs/backgrounds.md), and with nothing of the caller's to draw there is no |
| 30 |
* bitmap and the widget keeps its plain surface. |
| 31 |
*/ |
| 32 |
internal object SkyBitmap { |
| 33 |
/** Steps to let a still scene settle; it says when it has. */ |
| 34 |
private const val SETTLE_STEPS = 240 |
| 35 |
private const val DT = 1f / 30f |
| 36 |
|
| 37 |
/** Whether there is anything of the caller's to draw. */ |
| 38 |
fun drawable(sky: UserSky?): Boolean = sky != null && sky.repos.isNotEmpty() && skyEvents(sky).isNotEmpty() |
| 39 |
|
| 40 |
/** The scene's colours, as the app's backdrop has them for each theme: mark, lit, link, ok, warn, bad. */ |
| 41 |
fun colours(dark: Boolean): List<Float> { |
| 42 |
val (mark, lit, link) = if (dark) { |
| 43 |
Triple(Color(0, 224, 240).copy(alpha = 0.16f), Color(0, 240, 255).copy(alpha = 0.9f), Color(0, 240, 255).copy(alpha = 0.35f)) |
| 44 |
} else { |
| 45 |
Triple(Color(95, 106, 129).copy(alpha = 0.22f), Color(0, 100, 224).copy(alpha = 0.75f), Color(0, 100, 224).copy(alpha = 0.25f)) |
| 46 |
} |
| 47 |
val status = if (dark) listOf(DarkOk, DarkWarn, DarkBad) else listOf(LightOk, LightWarn, LightBad) |
| 48 |
return (listOf(mark, lit, link) + status).flatMap { listOf(it.red, it.green, it.blue, it.alpha) } |
| 49 |
} |
| 50 |
|
| 51 |
/** The settled frame of Your sky at [widthDp] × [heightDp], or null when there is nothing to draw. */ |
| 52 |
fun frame(sky: UserSky?, widthDp: Float, heightDp: Float, dark: Boolean): Frame? { |
| 53 |
if (!drawable(sky) || widthDp <= 0f || heightDp <= 0f) return null |
| 54 |
val scene = Scene.sky(sky!!.repos, skyEvents(sky), widthDp, heightDp, true) |
| 55 |
return try { |
| 56 |
scene.colours(colours(dark)) |
| 57 |
var n = 0 |
| 58 |
while (n < SETTLE_STEPS && scene.step(DT)) n++ |
| 59 |
Frame.of(scene.frame()).takeIf { it.marks.isNotEmpty() } |
| 60 |
} finally { |
| 61 |
scene.close() |
| 62 |
} |
| 63 |
} |
| 64 |
|
| 65 |
/** [frame] drawn into a bitmap of [widthPx] × [heightPx] pixels, [density] pixels to the dp. */ |
| 66 |
fun render(frame: Frame, widthPx: Int, heightPx: Int, density: Float, dark: Boolean): Bitmap { |
| 67 |
val bitmap = Bitmap.createBitmap(widthPx, heightPx, Bitmap.Config.ARGB_8888) |
| 68 |
draw(Canvas(bitmap), frame, density, dark) |
| 69 |
return bitmap |
| 70 |
} |
| 71 |
|
| 72 |
private fun draw(canvas: Canvas, f: Frame, density: Float, dark: Boolean) { |
| 73 |
val line = Paint(Paint.ANTI_ALIAS_FLAG).apply { strokeWidth = density; style = Paint.Style.STROKE } |
| 74 |
val l = f.links |
| 75 |
var i = 0 |
| 76 |
while (i + 12 <= l.size) { |
| 77 |
val (ax, ay, bx, by) = listOf(l[i], l[i + 1], l[i + 6], l[i + 7]).map { it * density } |
| 78 |
val ca = straight(l[i + 2], l[i + 3], l[i + 4], l[i + 5]).toArgb() |
| 79 |
val cb = straight(l[i + 8], l[i + 9], l[i + 10], l[i + 11]).toArgb() |
| 80 |
line.shader = LinearGradient(ax, ay, bx, by, ca, cb, Shader.TileMode.CLAMP) |
| 81 |
canvas.drawLine(ax, ay, bx, by, line) |
| 82 |
i += 12 |
| 83 |
} |
| 84 |
val fill = Paint(Paint.ANTI_ALIAS_FLAG) |
| 85 |
val ring = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE } |
| 86 |
val m = f.marks |
| 87 |
i = 0 |
| 88 |
while (i + 8 <= m.size) { |
| 89 |
val (x, y, s) = Triple(m[i] * density, m[i + 1] * density, m[i + 2] * density) |
| 90 |
val colour = straight(m[i + 4], m[i + 5], m[i + 6], m[i + 7]) |
| 91 |
// On a dark widget a lit mark glows a little, as in the app. |
| 92 |
if (dark && colour.alpha > 0.5f) { |
| 93 |
fill.color = colour.copy(alpha = colour.alpha * 0.14f).toArgb() |
| 94 |
canvas.drawCircle(x, y, s * 2.6f, fill) |
| 95 |
} |
| 96 |
fill.color = colour.toArgb() |
| 97 |
ring.color = colour.toArgb() |
| 98 |
when (Math.round(m[i + 3])) { |
| 99 |
0 -> canvas.drawCircle(x, y, s, fill) // a push: a dot |
| 100 |
1 -> canvas.drawPath(polygon(x, y, DIAMOND, s / 0.85f), fill) // a proposal: a diamond |
| 101 |
2 -> canvas.drawCircle(x, y, s * 0.72f, ring.apply { strokeWidth = s * 0.56f }) // an issue: a ring |
| 102 |
3 -> canvas.drawPath(polygon(x, y, STAR, s), fill) // a release: a star |
| 103 |
4 -> canvas.drawCircle(x, y, s * 0.85f, ring.apply { strokeWidth = s * 0.24f }) // a repository: a thin ring |
| 104 |
else -> { // a Flow run: a square |
| 105 |
val h = s / 1.15f |
| 106 |
canvas.drawRect(x - h, y - h, x + h, y + h, fill) |
| 107 |
} |
| 108 |
} |
| 109 |
i += 8 |
| 110 |
} |
| 111 |
} |
| 112 |
|
| 113 |
private val DIAMOND = floatArrayOf(1f, 0f, 0f, 1f, -1f, 0f, 0f, -1f) |
| 114 |
|
| 115 |
private fun polygon(x: Float, y: Float, unit: FloatArray, r: Float): Path = Path().apply { |
| 116 |
moveTo(x + unit[0] * r, y + unit[1] * r) |
| 117 |
var j = 2 |
| 118 |
while (j < unit.size) { |
| 119 |
lineTo(x + unit[j] * r, y + unit[j + 1] * r) |
| 120 |
j += 2 |
| 121 |
} |
| 122 |
close() |
| 123 |
} |
| 124 |
} |