package me.khodak.claudeusage import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF import android.graphics.Typeface import kotlin.math.cos import kotlin.math.sin /** * Draws a usage gauge as a circular ring Bitmap: rounded track ring + colored progress arc * (sweeping clockwise from 12 o'clock) + an optional radial pace tick + center percentage and * label. The ring counterpart to [BarRenderer] — same Bitmap contract, so it drops into both the * home-screen widget (setImageViewBitmap) and the in-app card and renders identically. * * Design mirrors hamed-elfayome/Claude-Usage-Tracker's ring gauges, using this app's existing * palette (SESSION_FILL clay / WEEKLY_FILL periwinkle, #252525 track, white pace marker). */ object RingRenderer { private const val TRACK_COLOR = 0xFF252525.toInt() private const val CENTER_TEXT_COLOR = 0xFFFFFFFF.toInt() private const val LABEL_TEXT_COLOR = 0xFF999999.toInt() /** * @param usedPct 0..100 fill of the ring * @param markerPct where you "should be" right now (pace), or null for no tick * @param fillColor ARGB progress-arc color (e.g. SESSION_FILL / WEEKLY_FILL) * @param markerColor ARGB pace-tick color, or null * @param centerText big text in the middle, e.g. "47%" (caller formats it) * @param labelText small caption under the number, e.g. "SESSION" (null hides it) * @param sizePx square bitmap edge; ring scales to fit * @param strokePx ring thickness */ fun render( usedPct: Int, markerPct: Int?, fillColor: Int, markerColor: Int?, centerText: String, labelText: String? = null, sizePx: Int = 360, strokePx: Float = 36f ): Bitmap { val bmp = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888) val canvas = Canvas(bmp) val cx = sizePx / 2f val cy = sizePx / 2f val pad = strokePx / 2f + 2f val arcRect = RectF(pad, pad, sizePx - pad, sizePx - pad) val radius = (sizePx - strokePx) / 2f - 2f // Track ring (full circle). val track = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeWidth = strokePx strokeCap = Paint.Cap.ROUND color = TRACK_COLOR } canvas.drawArc(arcRect, 0f, 360f, false, track) // Progress arc — clockwise from 12 o'clock (-90°). val pct = usedPct.coerceIn(0, 100) if (pct > 0) { val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeWidth = strokePx strokeCap = Paint.Cap.ROUND color = fillColor } canvas.drawArc(arcRect, -90f, pct / 100f * 360f, false, fill) } // Pace tick — a short radial mark across the ring at the "should be here" angle. if (markerPct != null && markerColor != null) { val m = markerPct.coerceIn(0, 100) val ang = Math.toRadians((-90f + m / 100f * 360f).toDouble()) val rInner = radius - strokePx / 2f - 1f val rOuter = radius + strokePx / 2f + 1f val tick = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeWidth = strokePx * 0.18f strokeCap = Paint.Cap.ROUND color = markerColor } canvas.drawLine( cx + (rInner * cos(ang)).toFloat(), cy + (rInner * sin(ang)).toFloat(), cx + (rOuter * cos(ang)).toFloat(), cy + (rOuter * sin(ang)).toFloat(), tick ) } // Center percentage. val hasLabel = !labelText.isNullOrEmpty() val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = CENTER_TEXT_COLOR textAlign = Paint.Align.CENTER textSize = sizePx * 0.27f typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } val vfm = valuePaint.fontMetrics val valueBaselineShift = -(vfm.ascent + vfm.descent) / 2f val valueY = cy + valueBaselineShift - (if (hasLabel) sizePx * 0.07f else 0f) canvas.drawText(centerText, cx, valueY, valuePaint) if (hasLabel) { val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = LABEL_TEXT_COLOR textAlign = Paint.Align.CENTER textSize = sizePx * 0.105f letterSpacing = 0.12f typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } canvas.drawText(labelText!!.uppercase(), cx, cy + sizePx * 0.17f, labelPaint) } return bmp } }