OXYGENHUB

Coverage / app/src/main/java/io/oxygenhub/android/widget/SkyBitmap.kt 12.1%

7 of 58 lines run, 3 of 7 functions, 14 of 46 branches · run #1664 on main · run missed

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
}