arblant
railway-clean-ip-scanner-android / app / src / main / java / com / arblant / railwayipscanner / UiKit.kt
UiKit.kt96 lines · 3.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
package com.arblant.railwayipscanner

import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.ForegroundColorSpan
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils

/**
 * Shared row/chip presentation for the results ledger, the workbench
 * and the IP picker — one component grammar everywhere (spec).
 */
object UiKit {

    fun dp(context: Context, value: Float): Int =
        (value * context.resources.displayMetrics.density + 0.5f).toInt()

    /** Latency tiers (spec): fast <120, mid 120–300, slow 300–600, poor >600. */
    fun tierColorRes(delayMs: Long): Int = when {
        delayMs < 120 -> R.color.latency_fast
        delayMs <= 300 -> R.color.latency_mid
        delayMs <= 600 -> R.color.latency_slow
        else -> R.color.latency_poor
    }

    fun tierColor(context: Context, delayMs: Long): Int =
        ContextCompat.getColor(context, tierColorRes(delayMs))

    private fun chipBackground(context: Context, color: Int): GradientDrawable {
        val bg = GradientDrawable()
        bg.shape = GradientDrawable.RECTANGLE
        bg.cornerRadius = dp(context, 7f).toFloat()
        bg.setColor(ColorUtils.setAlphaComponent(color, 36))   // 14% fill
        bg.setStroke(dp(context, 1f).coerceAtLeast(1), ColorUtils.setAlphaComponent(color, 89)) // 35% edge
        return bg
    }

    /** Live latency chip: tier-tinted fill/edge/text, fixed rail width. */
    fun styleLatencyChip(view: TextView, delayMs: Long) {
        val color = tierColor(view.context, delayMs)
        view.background = chipBackground(view.context, color)
        view.setTextColor(color)
        view.text = "$delayMs ms"
    }

    /** Measuring state: three quiet dots in tertiary ink. */
    fun styleMeasuringChip(view: TextView) {
        val ctx = view.context
        val ink = ContextCompat.getColor(ctx, R.color.ink_3)
        val bg = GradientDrawable()
        bg.shape = GradientDrawable.RECTANGLE
        bg.cornerRadius = dp(ctx, 7f).toFloat()
        bg.setColor(ContextCompat.getColor(ctx, R.color.panel_raised))
        bg.setStroke(dp(ctx, 1f).coerceAtLeast(1), ContextCompat.getColor(ctx, R.color.border))
        view.background = bg
        view.setTextColor(ink)
        view.text = "• • •"
    }

    /** Dead state after a failed re-test: poor tier, plain word. */
    fun styleDeadChip(view: TextView) {
        val color = ContextCompat.getColor(view.context, R.color.latency_poor)
        view.background = chipBackground(view.context, color)
        view.setTextColor(color)
        view.text = "Dead"
    }

    /** Stale state: the assigned IP vanished from the latest scan. */
    fun styleStaleChip(view: TextView) {
        val color = ContextCompat.getColor(view.context, R.color.latency_slow)
        view.background = chipBackground(view.context, color)
        view.setTextColor(color)
        view.text = "stale"
    }

    /**
     * Typeset IP: bright octets, dimmed dots (spec craft detail #1).
     * Tabular mono comes from the view's fontFamily/fontFeatureSettings.
     */
    fun dimmedIp(context: Context, ip: String): SpannableString {
        val span = SpannableString(ip)
        val dotColor = ContextCompat.getColor(context, R.color.ink_3)
        for (i in ip.indices) {
            if (ip[i] == '.') {
                span.setSpan(
                    ForegroundColorSpan(dotColor), i, i + 1,
                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
                )
            }
        }
        return span
    }
}