diff --git a/android/uhabits-android/src/androidTest/assets/fontawesome-webfont.ttf b/android/uhabits-android/src/androidTest/assets/fontawesome-webfont.ttf new file mode 100644 index 000000000..e89738de5 Binary files /dev/null and b/android/uhabits-android/src/androidTest/assets/fontawesome-webfont.ttf differ diff --git a/android/uhabits-android/src/androidTest/assets/views/CanvasTest.png b/android/uhabits-android/src/androidTest/assets/views/CanvasTest.png new file mode 100644 index 000000000..75b3d8f20 Binary files /dev/null and b/android/uhabits-android/src/androidTest/assets/views/CanvasTest.png differ diff --git a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderTransparent.png b/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderTransparent.png index 212260482..8cfb3ce13 100644 Binary files a/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderTransparent.png and b/android/uhabits-android/src/androidTest/assets/views/common/BarChart/renderTransparent.png differ diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/platform/gui/AndroidCanvasTest.kt b/android/uhabits-android/src/androidTest/java/org/isoron/platform/gui/AndroidCanvasTest.kt new file mode 100644 index 000000000..4bbb59b87 --- /dev/null +++ b/android/uhabits-android/src/androidTest/java/org/isoron/platform/gui/AndroidCanvasTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2016-2020 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import android.graphics.Bitmap +import org.isoron.uhabits.BaseViewTest +import org.junit.Test + +class AndroidCanvasTest : BaseViewTest() { + @Test + fun testDrawTestImage() { + similarityCutoff = 0.0005 + val bmp = Bitmap.createBitmap(1000, 800, Bitmap.Config.ARGB_8888) + val canvas = AndroidCanvas() + canvas.context = testContext + canvas.density = 2.0 + canvas.innerCanvas = android.graphics.Canvas(bmp) + canvas.innerBitmap = bmp + canvas.drawTestImage() + assertRenders(bmp, "CanvasTest.png") + } +} diff --git a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java index e56ae3561..cc84951ec 100644 --- a/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java +++ b/android/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java @@ -47,11 +47,14 @@ public class BaseViewTest extends BaseAndroidTest protected void assertRenders(View view, String expectedImagePath) throws IOException { - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - expectedImagePath = "views/" + expectedImagePath; Bitmap actual = renderView(view); if(actual == null) throw new IllegalStateException("actual is null"); + assertRenders(actual, expectedImagePath); + } + protected void assertRenders(Bitmap actual, String expectedImagePath) throws IOException { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + expectedImagePath = "views/" + expectedImagePath; try { Bitmap expected = getBitmapFromAssets(expectedImagePath); @@ -145,7 +148,7 @@ public class BaseViewTest extends BaseAndroidTest } } - distance /= (0xff * 16) * b1.getWidth() * b1.getHeight(); + distance /= 255.0 * 16 * b1.getWidth() * b1.getHeight(); return distance; } diff --git a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidCanvas.kt b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidCanvas.kt new file mode 100644 index 000000000..6cd92fb9a --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidCanvas.kt @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2016-2020 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.Typeface +import android.text.TextPaint +import android.util.AttributeSet +import android.view.View +import org.isoron.uhabits.utils.InterfaceUtils.getFontAwesome + +class AndroidCanvas : Canvas { + + lateinit var innerCanvas: android.graphics.Canvas + lateinit var context: Context + var innerBitmap: Bitmap? = null + var density = 1.0 + var paint = Paint().apply { + isAntiAlias = true + } + var textPaint = TextPaint().apply { + isAntiAlias = true + } + var textBounds = Rect() + + private fun Double.toDp() = (this * density).toFloat() + + override fun setColor(color: Color) { + paint.color = color.toInt() + textPaint.color = color.toInt() + } + + override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) { + innerCanvas.drawLine( + x1.toDp(), + y1.toDp(), + x2.toDp(), + y2.toDp(), + paint, + ) + } + + override fun drawText(text: String, x: Double, y: Double) { + textPaint.getTextBounds(text, 0, text.length, textBounds) + innerCanvas.drawText( + text, + x.toDp(), + y.toDp() - textBounds.exactCenterY(), + textPaint, + ) + } + + override fun fillRect(x: Double, y: Double, width: Double, height: Double) { + paint.style = Paint.Style.FILL + rect(x, y, width, height) + } + + override fun drawRect(x: Double, y: Double, width: Double, height: Double) { + paint.style = Paint.Style.STROKE + rect(x, y, width, height) + } + + private fun rect(x: Double, y: Double, width: Double, height: Double) { + innerCanvas.drawRect( + x.toDp(), + y.toDp(), + (x + width).toDp(), + (y + height).toDp(), + paint, + ) + } + + override fun getHeight(): Double { + return innerCanvas.height / density + } + + override fun getWidth(): Double { + return innerCanvas.width / density + } + + override fun setFont(font: Font) { + textPaint.typeface = when (font) { + Font.REGULAR -> Typeface.DEFAULT + Font.BOLD -> Typeface.DEFAULT_BOLD + Font.FONT_AWESOME -> getFontAwesome(context) + } + } + + override fun setFontSize(size: Double) { + textPaint.textSize = size.toDp() * 1.07f + } + + override fun setStrokeWidth(size: Double) { + paint.strokeWidth = size.toDp() + } + + override fun fillArc( + centerX: Double, + centerY: Double, + radius: Double, + startAngle: Double, + swipeAngle: Double, + ) { + paint.style = Paint.Style.FILL + innerCanvas.drawArc( + (centerX - radius).toDp(), + (centerY - radius).toDp(), + (centerX + radius).toDp(), + (centerY + radius).toDp(), + -startAngle.toFloat(), + -swipeAngle.toFloat(), + true, + paint, + ) + } + + override fun fillCircle( + centerX: Double, + centerY: Double, + radius: Double, + ) { + paint.style = Paint.Style.FILL + innerCanvas.drawCircle(centerX.toDp(), centerY.toDp(), radius.toDp(), paint) + } + + override fun setTextAlign(align: TextAlign) { + textPaint.textAlign = when (align) { + TextAlign.LEFT -> Paint.Align.LEFT + TextAlign.CENTER -> Paint.Align.CENTER + TextAlign.RIGHT -> Paint.Align.RIGHT + } + } + + override fun toImage(): Image { + val bmp = innerBitmap ?: throw UnsupportedOperationException() + return AndroidImage(bmp) + } +} + +class AndroidCanvasTestView(context: Context, attrs: AttributeSet) : View(context, attrs) { + val canvas = AndroidCanvas() + + override fun onDraw(canvas: android.graphics.Canvas) { + this.canvas.context = context + this.canvas.innerCanvas = canvas + this.canvas.density = resources.displayMetrics.density.toDouble() + this.canvas.drawTestImage() + } +} diff --git a/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidImage.kt b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidImage.kt new file mode 100644 index 000000000..18e81ce7b --- /dev/null +++ b/android/uhabits-android/src/main/java/org/isoron/platform/gui/AndroidImage.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2016-2020 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import android.graphics.Bitmap +import kotlin.math.roundToInt + +class AndroidImage(private val bmp: Bitmap) : Image { + override val width: Int + get() = bmp.width + + override val height: Int + get() = bmp.height + + override fun getPixel(x: Int, y: Int): Color { + return Color(bmp.getPixel(x, y)) + } + + override fun setPixel(x: Int, y: Int, color: Color) { + bmp.setPixel(x, y, color.toInt()) + } + + override suspend fun export(path: String) { + TODO("Not yet implemented") + } +} + +public fun Color.toInt(): Int { + return android.graphics.Color.argb( + (255 * this.alpha).roundToInt(), + (255 * this.red).roundToInt(), + (255 * this.green).roundToInt(), + (255 * this.blue).roundToInt(), + ) +} diff --git a/android/uhabits-android/src/main/res/layout/canvas_test.xml b/android/uhabits-android/src/main/res/layout/canvas_test.xml new file mode 100644 index 000000000..2d2cee167 --- /dev/null +++ b/android/uhabits-android/src/main/res/layout/canvas_test.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/android/uhabits-core/assets/main/fonts/FontAwesome.ttf b/android/uhabits-core/assets/main/fonts/FontAwesome.ttf new file mode 100644 index 000000000..e89738de5 Binary files /dev/null and b/android/uhabits-core/assets/main/fonts/FontAwesome.ttf differ diff --git a/android/uhabits-core/assets/main/fonts/NotoSans-Bold.ttf b/android/uhabits-core/assets/main/fonts/NotoSans-Bold.ttf new file mode 100755 index 000000000..987da8c9b Binary files /dev/null and b/android/uhabits-core/assets/main/fonts/NotoSans-Bold.ttf differ diff --git a/android/uhabits-core/assets/main/fonts/NotoSans-Regular.ttf b/android/uhabits-core/assets/main/fonts/NotoSans-Regular.ttf new file mode 100755 index 000000000..79a438a53 Binary files /dev/null and b/android/uhabits-core/assets/main/fonts/NotoSans-Regular.ttf differ diff --git a/android/uhabits-core/assets/test/components/BarChart/base.png b/android/uhabits-core/assets/test/components/BarChart/base.png new file mode 100644 index 000000000..4bb551c99 Binary files /dev/null and b/android/uhabits-core/assets/test/components/BarChart/base.png differ diff --git a/android/uhabits-core/assets/test/components/CalendarChart/base.png b/android/uhabits-core/assets/test/components/CalendarChart/base.png new file mode 100644 index 000000000..762877164 Binary files /dev/null and b/android/uhabits-core/assets/test/components/CalendarChart/base.png differ diff --git a/android/uhabits-core/assets/test/components/CalendarChart/scroll.png b/android/uhabits-core/assets/test/components/CalendarChart/scroll.png new file mode 100644 index 000000000..16744a297 Binary files /dev/null and b/android/uhabits-core/assets/test/components/CalendarChart/scroll.png differ diff --git a/android/uhabits-core/assets/test/components/CanvasTest.png b/android/uhabits-core/assets/test/components/CanvasTest.png new file mode 100644 index 000000000..75b3d8f20 Binary files /dev/null and b/android/uhabits-core/assets/test/components/CanvasTest.png differ diff --git a/android/uhabits-core/assets/test/components/CheckmarkButton/explicit.png b/android/uhabits-core/assets/test/components/CheckmarkButton/explicit.png new file mode 100644 index 000000000..c2b66f74e Binary files /dev/null and b/android/uhabits-core/assets/test/components/CheckmarkButton/explicit.png differ diff --git a/android/uhabits-core/assets/test/components/CheckmarkButton/implicit.png b/android/uhabits-core/assets/test/components/CheckmarkButton/implicit.png new file mode 100644 index 000000000..acc067024 Binary files /dev/null and b/android/uhabits-core/assets/test/components/CheckmarkButton/implicit.png differ diff --git a/android/uhabits-core/assets/test/components/CheckmarkButton/unchecked.png b/android/uhabits-core/assets/test/components/CheckmarkButton/unchecked.png new file mode 100644 index 000000000..283f32d40 Binary files /dev/null and b/android/uhabits-core/assets/test/components/CheckmarkButton/unchecked.png differ diff --git a/android/uhabits-core/assets/test/components/HabitListHeader/light.png b/android/uhabits-core/assets/test/components/HabitListHeader/light.png new file mode 100644 index 000000000..c146b916d Binary files /dev/null and b/android/uhabits-core/assets/test/components/HabitListHeader/light.png differ diff --git a/android/uhabits-core/assets/test/components/NumberButton/render_above.png b/android/uhabits-core/assets/test/components/NumberButton/render_above.png new file mode 100644 index 000000000..4673ef0f3 Binary files /dev/null and b/android/uhabits-core/assets/test/components/NumberButton/render_above.png differ diff --git a/android/uhabits-core/assets/test/components/NumberButton/render_below.png b/android/uhabits-core/assets/test/components/NumberButton/render_below.png new file mode 100644 index 000000000..c3ffb68de Binary files /dev/null and b/android/uhabits-core/assets/test/components/NumberButton/render_below.png differ diff --git a/android/uhabits-core/assets/test/components/NumberButton/render_zero.png b/android/uhabits-core/assets/test/components/NumberButton/render_zero.png new file mode 100644 index 000000000..235de2f62 Binary files /dev/null and b/android/uhabits-core/assets/test/components/NumberButton/render_zero.png differ diff --git a/android/uhabits-core/assets/test/components/Ring/draw1.png b/android/uhabits-core/assets/test/components/Ring/draw1.png new file mode 100644 index 000000000..d5e80f455 Binary files /dev/null and b/android/uhabits-core/assets/test/components/Ring/draw1.png differ diff --git a/android/uhabits-core/assets/test/hello.txt b/android/uhabits-core/assets/test/hello.txt new file mode 100644 index 000000000..e432ffe94 --- /dev/null +++ b/android/uhabits-core/assets/test/hello.txt @@ -0,0 +1,2 @@ +Hello World! +This is a resource. \ No newline at end of file diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/Canvas.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Canvas.kt new file mode 100644 index 000000000..9a492c958 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Canvas.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +enum class TextAlign { + LEFT, CENTER, RIGHT +} + +enum class Font { + REGULAR, + BOLD, + FONT_AWESOME +} + +interface Canvas { + fun setColor(color: Color) + fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) + fun drawText(text: String, x: Double, y: Double) + fun fillRect(x: Double, y: Double, width: Double, height: Double) + fun drawRect(x: Double, y: Double, width: Double, height: Double) + fun getHeight(): Double + fun getWidth(): Double + fun setFont(font: Font) + fun setFontSize(size: Double) + fun setStrokeWidth(size: Double) + fun fillArc( + centerX: Double, + centerY: Double, + radius: Double, + startAngle: Double, + swipeAngle: Double + ) + fun fillCircle(centerX: Double, centerY: Double, radius: Double) + fun setTextAlign(align: TextAlign) + fun toImage(): Image + + fun drawTestImage() { + // Draw grey background + setColor(Color(0x303030)) + fillRect(0.0, 0.0, 500.0, 400.0) + + // Draw center rectangle + setColor(Color(0x606060)) + setStrokeWidth(25.0) + drawRect(100.0, 100.0, 300.0, 200.0) + + // Draw squares, circles and arcs + setColor(Color(0xFFFF00)) + setStrokeWidth(1.0) + drawRect(0.0, 0.0, 100.0, 100.0) + fillCircle(50.0, 50.0, 30.0) + drawRect(0.0, 100.0, 100.0, 100.0) + fillArc(50.0, 150.0, 30.0, 90.0, 135.0) + drawRect(0.0, 200.0, 100.0, 100.0) + fillArc(50.0, 250.0, 30.0, 90.0, -135.0) + drawRect(0.0, 300.0, 100.0, 100.0) + fillArc(50.0, 350.0, 30.0, 45.0, 90.0) + + // Draw two red crossing lines + setColor(Color(0xFF0000)) + setStrokeWidth(2.0) + drawLine(0.0, 0.0, 500.0, 400.0) + drawLine(500.0, 0.0, 0.0, 400.0) + + // Draw text + setFont(Font.BOLD) + setFontSize(50.0) + setColor(Color(0x00FF00)) + setTextAlign(TextAlign.CENTER) + drawText("HELLO", 250.0, 100.0) + setTextAlign(TextAlign.RIGHT) + drawText("HELLO", 250.0, 150.0) + setTextAlign(TextAlign.LEFT) + drawText("HELLO", 250.0, 200.0) + + // Draw FontAwesome icon + setFont(Font.FONT_AWESOME) + drawText(FontAwesome.CHECK, 250.0, 300.0) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/Colors.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Colors.kt new file mode 100644 index 000000000..ee11e2c0b --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Colors.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +data class PaletteColor(val index: Int) + +data class Color( + val red: Double, + val green: Double, + val blue: Double, + val alpha: Double +) { + + val luminosity: Double + get() { + return 0.21 * red + 0.72 * green + 0.07 * blue + } + + constructor(rgb: Int) : this( + ((rgb shr 16) and 0xFF) / 255.0, + ((rgb shr 8) and 0xFF) / 255.0, + ((rgb shr 0) and 0xFF) / 255.0, + 1.0 + ) + + fun blendWith(other: Color, weight: Double): Color { + return Color( + red * (1 - weight) + other.red * weight, + green * (1 - weight) + other.green * weight, + blue * (1 - weight) + other.blue * weight, + alpha * (1 - weight) + other.alpha * weight + ) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/Component.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Component.kt new file mode 100644 index 000000000..8572f048e --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Component.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +interface Component { + fun draw(canvas: Canvas) +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/FontAwesome.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/FontAwesome.kt new file mode 100644 index 000000000..add792d90 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/FontAwesome.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +class FontAwesome { + companion object { + val CHECK = "\uf00c" + val TIMES = "\uf00d" + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/Image.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Image.kt new file mode 100644 index 000000000..660ede1b0 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/Image.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import kotlin.math.abs +import kotlin.math.min + +interface Image { + val width: Int + val height: Int + + fun getPixel(x: Int, y: Int): Color + fun setPixel(x: Int, y: Int, color: Color) + + suspend fun export(path: String) + + fun diff(other: Image) { + if (width != other.width) error("Width must match: $width !== ${other.width}") + if (height != other.height) error("Height must match: $height !== ${other.height}") + + for (x in 0 until width) { + for (y in 0 until height) { + val p1 = getPixel(x, y) + var l = 1.0 + for (dx in -2..2) { + if (x + dx < 0 || x + dx >= width) continue + for (dy in -2..2) { + if (y + dy < 0 || y + dy >= height) continue + val p2 = other.getPixel(x + dx, y + dy) + l = min(l, abs(p1.luminosity - p2.luminosity)) + } + } + setPixel(x, y, Color(l, l, l, 1.0)) + } + } + } + + val averageLuminosity: Double + get() { + var luminosity = 0.0 + for (x in 0 until width) { + for (y in 0 until height) { + luminosity += getPixel(x, y).luminosity + } + } + return luminosity / (width * height) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaCanvas.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaCanvas.kt new file mode 100644 index 000000000..73034cad4 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaCanvas.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import kotlinx.coroutines.runBlocking +import org.isoron.platform.io.JavaFileOpener +import org.isoron.platform.io.JavaResourceFile +import java.awt.BasicStroke +import java.awt.RenderingHints.KEY_ANTIALIASING +import java.awt.RenderingHints.KEY_FRACTIONALMETRICS +import java.awt.RenderingHints.KEY_TEXT_ANTIALIASING +import java.awt.RenderingHints.VALUE_ANTIALIAS_ON +import java.awt.RenderingHints.VALUE_FRACTIONALMETRICS_ON +import java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON +import java.awt.font.FontRenderContext +import java.awt.image.BufferedImage +import kotlin.math.roundToInt + +class JavaCanvas( + val image: BufferedImage, + val pixelScale: Double = 2.0 +) : Canvas { + + override fun toImage(): Image { + return JavaImage(image) + } + + private val frc = FontRenderContext(null, true, true) + private var fontSize = 12.0 + private var font = Font.REGULAR + private var textAlign = TextAlign.CENTER + val widthPx = image.width + val heightPx = image.height + val g2d = image.createGraphics() + + private val NOTO_REGULAR_FONT = createFont("fonts/NotoSans-Regular.ttf") + private val NOTO_BOLD_FONT = createFont("fonts/NotoSans-Bold.ttf") + private val FONT_AWESOME_FONT = createFont("fonts/FontAwesome.ttf") + + init { + g2d.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON) + g2d.setRenderingHint(KEY_TEXT_ANTIALIASING, VALUE_TEXT_ANTIALIAS_ON) + g2d.setRenderingHint(KEY_FRACTIONALMETRICS, VALUE_FRACTIONALMETRICS_ON) + updateFont() + } + + private fun toPixel(x: Double): Int { + return (pixelScale * x).toInt() + } + + private fun toDp(x: Int): Double { + return x / pixelScale + } + + override fun setColor(color: Color) { + g2d.color = java.awt.Color( + color.red.toFloat(), + color.green.toFloat(), + color.blue.toFloat(), + color.alpha.toFloat() + ) + } + + override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) { + g2d.drawLine(toPixel(x1), toPixel(y1), toPixel(x2), toPixel(y2)) + } + + override fun drawText(text: String, x: Double, y: Double) { + updateFont() + val bounds = g2d.font.getStringBounds(text, frc) + val bWidth = bounds.width.roundToInt() + val bHeight = bounds.height.roundToInt() + val bx = bounds.x.roundToInt() + val by = bounds.y.roundToInt() + + if (textAlign == TextAlign.CENTER) { + g2d.drawString( + text, + toPixel(x) - bx - bWidth / 2, + toPixel(y) - by - bHeight / 2 + ) + } else if (textAlign == TextAlign.LEFT) { + g2d.drawString( + text, + toPixel(x) - bx, + toPixel(y) - by - bHeight / 2 + ) + } else { + g2d.drawString( + text, + toPixel(x) - bx - bWidth, + toPixel(y) - by - bHeight / 2 + ) + } + } + + override fun fillRect(x: Double, y: Double, width: Double, height: Double) { + g2d.fillRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height)) + } + + override fun drawRect(x: Double, y: Double, width: Double, height: Double) { + g2d.drawRect(toPixel(x), toPixel(y), toPixel(width), toPixel(height)) + } + + override fun getHeight(): Double { + return toDp(heightPx) + } + + override fun getWidth(): Double { + return toDp(widthPx) + } + + override fun setFont(font: Font) { + this.font = font + updateFont() + } + + override fun setFontSize(size: Double) { + fontSize = size + updateFont() + } + + override fun setStrokeWidth(size: Double) { + g2d.stroke = BasicStroke((size * pixelScale).toFloat()) + } + + private fun updateFont() { + val size = (fontSize * pixelScale).toFloat() + g2d.font = when (font) { + Font.REGULAR -> NOTO_REGULAR_FONT.deriveFont(size) + Font.BOLD -> NOTO_BOLD_FONT.deriveFont(size) + Font.FONT_AWESOME -> FONT_AWESOME_FONT.deriveFont(size) + } + } + + override fun fillCircle(centerX: Double, centerY: Double, radius: Double) { + g2d.fillOval( + toPixel(centerX - radius), + toPixel(centerY - radius), + toPixel(radius * 2), + toPixel(radius * 2) + ) + } + + override fun fillArc( + centerX: Double, + centerY: Double, + radius: Double, + startAngle: Double, + swipeAngle: Double + ) { + + g2d.fillArc( + toPixel(centerX - radius), + toPixel(centerY - radius), + toPixel(radius * 2), + toPixel(radius * 2), + startAngle.roundToInt(), + swipeAngle.roundToInt() + ) + } + + override fun setTextAlign(align: TextAlign) { + this.textAlign = align + } + + private fun createFont(path: String) = runBlocking { + val file = JavaFileOpener().openResourceFile(path) as JavaResourceFile + if (!file.exists()) throw RuntimeException("File not found: ${file.path}") + java.awt.Font.createFont(0, file.stream()) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaImage.kt b/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaImage.kt new file mode 100644 index 000000000..ba8878d2a --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/gui/JavaImage.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import java.awt.image.BufferedImage +import java.io.File +import javax.imageio.ImageIO + +class JavaImage(val bufferedImage: BufferedImage) : Image { + override fun setPixel(x: Int, y: Int, color: Color) { + bufferedImage.setRGB( + x, + y, + java.awt.Color( + color.red.toFloat(), + color.green.toFloat(), + color.blue.toFloat() + ).rgb + ) + } + + override suspend fun export(path: String) { + val file = File(path) + file.parentFile.mkdirs() + ImageIO.write(bufferedImage, "png", file) + } + + override val width: Int + get() = bufferedImage.width + + override val height: Int + get() = bufferedImage.height + + override fun getPixel(x: Int, y: Int): Color { + return Color(bufferedImage.getRGB(x, y)) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/io/Files.kt b/android/uhabits-core/src/main/java/org/isoron/platform/io/Files.kt new file mode 100644 index 000000000..75aaf78a9 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/io/Files.kt @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.io + +import org.isoron.platform.gui.Image + +interface FileOpener { + /** + * Opens a file which was shipped bundled with the application, such as a + * migration file. + * + * The path is relative to the assets folder. For example, to open + * assets/main/migrations/09.sql you should provide migrations/09.sql + * as the path. + * + * This function always succeed, even if the file does not exist. + */ + fun openResourceFile(path: String): ResourceFile + + /** + * Opens a file which was not shipped with the application, such as + * databases and logs. + * + * The path is relative to the user folder. For example, if the application + * stores the user data at /home/user/.loop/ and you wish to open the file + * /home/user/.loop/crash.log, you should provide crash.log as the path. + * + * This function always succeed, even if the file does not exist. + */ + fun openUserFile(path: String): UserFile +} + +/** + * Represents a file that was created after the application was installed, as a + * result of some user action, such as databases and logs. + */ +interface UserFile { + /** + * Deletes the user file. If the file does not exist, nothing happens. + */ + suspend fun delete() + + /** + * Returns true if the file exists. + */ + suspend fun exists(): Boolean + + /** + * Returns the lines of the file. If the file does not exist, throws an + * exception. + */ + suspend fun lines(): List +} + +/** + * Represents a file that was shipped with the application, such as migration + * files or database templates. + */ +interface ResourceFile { + /** + * Copies the resource file to the specified user file. If the user file + * already exists, it is replaced. If not, a new file is created. + */ + suspend fun copyTo(dest: UserFile) + + /** + * Returns the lines of the resource file. If the file does not exist, + * throws an exception. + */ + suspend fun lines(): List + + /** + * Returns true if the file exists. + */ + suspend fun exists(): Boolean + + /** + * Loads resource file as an image. + */ + suspend fun toImage(): Image +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/io/JavaFiles.kt b/android/uhabits-core/src/main/java/org/isoron/platform/io/JavaFiles.kt new file mode 100644 index 000000000..269782d97 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/io/JavaFiles.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.io + +import org.isoron.platform.gui.Image +import org.isoron.platform.gui.JavaImage +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import javax.imageio.ImageIO + +@Suppress("NewApi") +class JavaResourceFile(val path: String) : ResourceFile { + private val javaPath: Path + get() { + val mainPath = Paths.get("assets/main/$path") + val testPath = Paths.get("assets/test/$path") + if (Files.exists(mainPath)) return mainPath + else return testPath + } + + override suspend fun exists(): Boolean { + return Files.exists(javaPath) + } + + override suspend fun lines(): List { + return Files.readAllLines(javaPath) + } + + override suspend fun copyTo(dest: UserFile) { + if (dest.exists()) dest.delete() + val destPath = (dest as JavaUserFile).path + destPath.toFile().parentFile?.mkdirs() + Files.copy(javaPath, destPath) + } + + fun stream(): InputStream { + return Files.newInputStream(javaPath) + } + + override suspend fun toImage(): Image { + return JavaImage(ImageIO.read(stream())) + } +} + +@Suppress("NewApi") +class JavaUserFile(val path: Path) : UserFile { + override suspend fun lines(): List { + return Files.readAllLines(path) + } + + override suspend fun exists(): Boolean { + return Files.exists(path) + } + + override suspend fun delete() { + Files.delete(path) + } +} + +@Suppress("NewApi") +class JavaFileOpener : FileOpener { + override fun openUserFile(path: String): UserFile { + val path = Paths.get("/tmp/$path") + return JavaUserFile(path) + } + + override fun openResourceFile(path: String): ResourceFile { + return JavaResourceFile(path) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/time/Dates.kt b/android/uhabits-core/src/main/java/org/isoron/platform/time/Dates.kt new file mode 100644 index 000000000..65bb1e3e7 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/time/Dates.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.time + +import kotlin.math.abs +import kotlin.math.ceil + +enum class DayOfWeek(val index: Int) { + SUNDAY(0), + MONDAY(1), + TUESDAY(2), + WEDNESDAY(3), + THURSDAY(4), + FRIDAY(5), + SATURDAY(6), +} + +data class Timestamp(val millisSince1970: Long) { + val localDate: LocalDate + get() { + val millisSince2000 = millisSince1970 - 946684800000 + val daysSince2000 = millisSince2000 / 86400000 + return LocalDate(daysSince2000.toInt()) + } +} + +data class LocalDate(val daysSince2000: Int) { + + var yearCache = -1 + var monthCache = -1 + var dayCache = -1 + + constructor(year: Int, month: Int, day: Int) : + this(daysSince2000(year, month, day)) + + val dayOfWeek: DayOfWeek + get() { + return when (daysSince2000 % 7) { + 0 -> DayOfWeek.SATURDAY + 1 -> DayOfWeek.SUNDAY + 2 -> DayOfWeek.MONDAY + 3 -> DayOfWeek.TUESDAY + 4 -> DayOfWeek.WEDNESDAY + 5 -> DayOfWeek.THURSDAY + else -> DayOfWeek.FRIDAY + } + } + + val timestamp: Timestamp + get() { + return Timestamp(946684800000 + daysSince2000.toLong() * 86400000) + } + + val year: Int + get() { + if (yearCache < 0) updateYearMonthDayCache() + return yearCache + } + + val month: Int + get() { + if (monthCache < 0) updateYearMonthDayCache() + return monthCache + } + + val day: Int + get() { + if (dayCache < 0) updateYearMonthDayCache() + return dayCache + } + + private fun updateYearMonthDayCache() { + var currYear = 2000 + var currDay = 0 + + while (true) { + val currYearLength = if (isLeapYear(currYear)) 366 else 365 + if (daysSince2000 < currDay + currYearLength) { + yearCache = currYear + break + } else { + currYear++ + currDay += currYearLength + } + } + + var currMonth = 1 + val monthOffset = if (isLeapYear(currYear)) leapOffset else nonLeapOffset + + while (true) { + if (daysSince2000 < currDay + monthOffset[currMonth]) { + monthCache = currMonth + break + } else { + currMonth++ + } + } + + currDay += monthOffset[currMonth - 1] + dayCache = daysSince2000 - currDay + 1 + } + + fun isOlderThan(other: LocalDate): Boolean { + return daysSince2000 < other.daysSince2000 + } + + fun isNewerThan(other: LocalDate): Boolean { + return daysSince2000 > other.daysSince2000 + } + + fun plus(days: Int): LocalDate { + return LocalDate(daysSince2000 + days) + } + + fun minus(days: Int): LocalDate { + return LocalDate(daysSince2000 - days) + } + + fun distanceTo(other: LocalDate): Int { + return abs(daysSince2000 - other.daysSince2000) + } +} + +interface LocalDateFormatter { + fun shortWeekdayName(date: LocalDate): String + fun shortMonthName(date: LocalDate): String +} + +private fun isLeapYear(year: Int): Boolean { + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 +} + +val leapOffset = arrayOf( + 0, 31, 60, 91, 121, 152, 182, + 213, 244, 274, 305, 335, 366 +) +val nonLeapOffset = arrayOf( + 0, 31, 59, 90, 120, 151, 181, + 212, 243, 273, 304, 334, 365 +) + +private fun daysSince2000(year: Int, month: Int, day: Int): Int { + + var result = 365 * (year - 2000) + result += ceil((year - 2000) / 4.0).toInt() + result -= ceil((year - 2000) / 100.0).toInt() + result += ceil((year - 2000) / 400.0).toInt() + if (isLeapYear(year)) { + result += leapOffset[month - 1] + } else { + result += nonLeapOffset[month - 1] + } + result += (day - 1) + return result +} diff --git a/android/uhabits-core/src/main/java/org/isoron/platform/time/JavaDates.kt b/android/uhabits-core/src/main/java/org/isoron/platform/time/JavaDates.kt new file mode 100644 index 000000000..91b59e427 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/platform/time/JavaDates.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.time + +import java.util.Calendar.DAY_OF_MONTH +import java.util.Calendar.DAY_OF_WEEK +import java.util.Calendar.HOUR_OF_DAY +import java.util.Calendar.LONG +import java.util.Calendar.MILLISECOND +import java.util.Calendar.MINUTE +import java.util.Calendar.MONTH +import java.util.Calendar.SECOND +import java.util.Calendar.SHORT +import java.util.Calendar.YEAR +import java.util.GregorianCalendar +import java.util.Locale +import java.util.TimeZone + +fun LocalDate.toGregorianCalendar(): GregorianCalendar { + val cal = GregorianCalendar() + cal.timeZone = TimeZone.getTimeZone("GMT") + cal.set(MILLISECOND, 0) + cal.set(SECOND, 0) + cal.set(MINUTE, 0) + cal.set(HOUR_OF_DAY, 0) + cal.set(YEAR, this.year) + cal.set(MONTH, this.month - 1) + cal.set(DAY_OF_MONTH, this.day) + return cal +} + +class JavaLocalDateFormatter(private val locale: Locale) : LocalDateFormatter { + override fun shortMonthName(date: LocalDate): String { + val cal = date.toGregorianCalendar() + val longName = cal.getDisplayName(MONTH, LONG, locale) + val shortName = cal.getDisplayName(MONTH, SHORT, locale) + + // For some locales, such as Japan, SHORT name is exceedingly short + return if (longName.length <= 3) longName else shortName + } + + override fun shortWeekdayName(date: LocalDate): String { + val cal = date.toGregorianCalendar() + return cal.getDisplayName(DAY_OF_WEEK, SHORT, locale) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/BarChart.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/BarChart.kt new file mode 100644 index 000000000..638a92a0e --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/BarChart.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.core.ui.views + +import org.isoron.platform.gui.Canvas +import org.isoron.platform.gui.Color +import org.isoron.platform.gui.Component +import org.isoron.platform.gui.TextAlign +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.LocalDateFormatter +import kotlin.math.floor +import kotlin.math.max +import kotlin.math.round + +class BarChart( + var theme: Theme, + var dateFormatter: LocalDateFormatter, +) : Component { + + // Data + var series = mutableListOf>() + var colors = mutableListOf() + var axis = listOf() + + // Style + var paddingTop = 20.0 + var paddingLeft = 0.0 + var paddingRight = 0.0 + var footerHeight = 40.0 + var barGroupMargin = 4.0 + var barMargin = 3.0 + var barWidth = 12.0 + var nGridlines = 6 + var backgroundColor = theme.cardBackgroundColor + + override fun draw(canvas: Canvas) { + val width = canvas.getWidth() + val height = canvas.getHeight() + + val nSeries = series.size + val barGroupWidth = 2 * barGroupMargin + nSeries * (barWidth + 2 * barMargin) + val safeWidth = width - paddingLeft - paddingRight + val nColumns = floor((safeWidth) / barGroupWidth).toInt() + val marginLeft = (safeWidth - nColumns * barGroupWidth) / 2 + val maxBarHeight = height - footerHeight - paddingTop + var maxValue = series.map { it.max()!! }.max()!! + maxValue = max(maxValue, 1.0) + + canvas.setColor(backgroundColor) + canvas.fillRect(0.0, 0.0, width, height) + + fun barGroupOffset(c: Int) = marginLeft + paddingLeft + + (c) * barGroupWidth + + fun barOffset(c: Int, s: Int) = barGroupOffset(c) + + barGroupMargin + + s * (barWidth + 2 * barMargin) + + barMargin + + fun drawColumn(s: Int, c: Int) { + val dataColumn = nColumns - c - 1 + val value = if (dataColumn < series[s].size) series[s][dataColumn] else 0.0 + if (value <= 0) return + val perc = value / maxValue + val barHeight = round(maxBarHeight * perc) + val x = barOffset(c, s) + val y = height - footerHeight - barHeight + canvas.setColor(colors[s]) + val r = round(barWidth * 0.15) + canvas.fillRect(x, y + r, barWidth, barHeight - r) + canvas.fillRect(x + r, y, barWidth - 2 * r, r) + canvas.fillCircle(x + r, y + r, r) + canvas.fillCircle(x + barWidth - r, y + r, r) + canvas.setFontSize(theme.smallTextSize) + canvas.setTextAlign(TextAlign.CENTER) + canvas.setColor(backgroundColor) + canvas.fillRect( + x - barMargin, + y - theme.smallTextSize * 1.25, + barWidth + 2 * barMargin, + theme.smallTextSize * 1.0 + ) + canvas.setColor(colors[s]) + canvas.drawText( + value.toShortString(), + x + barWidth / 2, + y - theme.smallTextSize * 0.80 + ) + } + + fun drawSeries(s: Int) { + for (c in 0 until nColumns) drawColumn(s, c) + } + + fun drawMajorGrid() { + canvas.setStrokeWidth(1.0) + if (nSeries > 1) { + canvas.setColor( + backgroundColor.blendWith( + theme.lowContrastTextColor, + 0.5 + ) + ) + for (c in 0 until nColumns - 1) { + val x = barGroupOffset(c) + canvas.drawLine(x, paddingTop, x, paddingTop + maxBarHeight) + } + } + for (k in 1 until nGridlines) { + val pct = 1.0 - (k.toDouble() / (nGridlines - 1)) + val y = paddingTop + maxBarHeight * pct + canvas.setColor(theme.lowContrastTextColor) + canvas.setStrokeWidth(0.5) + canvas.drawLine(0.0, y, width, y) + } + } + + fun drawAxis() { + val y = paddingTop + maxBarHeight + canvas.setColor(backgroundColor) + canvas.fillRect(0.0, y, width, height - y) + canvas.setColor(theme.lowContrastTextColor) + canvas.drawLine(0.0, y, width, y) + canvas.setColor(theme.mediumContrastTextColor) + canvas.setTextAlign(TextAlign.CENTER) + var prevMonth = -1 + var prevYear = -1 + val isLargeInterval = (axis[0].distanceTo(axis[1]) > 300) + + for (c in 0 until nColumns) { + val x = barGroupOffset(c) + val dataColumn = nColumns - c - 1 + if (dataColumn >= axis.size) continue + val date = axis[dataColumn] + if (isLargeInterval) { + canvas.drawText( + date.year.toString(), + x + barGroupWidth / 2, + y + theme.smallTextSize * 1.0 + ) + } else { + if (date.month != prevMonth) { + canvas.drawText( + dateFormatter.shortMonthName(date), + x + barGroupWidth / 2, + y + theme.smallTextSize * 1.0 + ) + } else { + canvas.drawText( + date.day.toString(), + x + barGroupWidth / 2, + y + theme.smallTextSize * 1.0 + ) + } + if (date.year != prevYear) { + canvas.drawText( + date.year.toString(), + x + barGroupWidth / 2, + y + theme.smallTextSize * 2.3 + ) + } + } + prevMonth = date.month + prevYear = date.year + } + } + + drawMajorGrid() + for (k in 0 until nSeries) drawSeries(k) + drawAxis() + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CalendarChart.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CalendarChart.kt new file mode 100644 index 000000000..2555e0d9e --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CalendarChart.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.core.ui.views + +import org.isoron.platform.gui.Canvas +import org.isoron.platform.gui.Color +import org.isoron.platform.gui.Component +import org.isoron.platform.gui.TextAlign +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.LocalDateFormatter +import kotlin.math.floor +import kotlin.math.round + +class CalendarChart( + var today: LocalDate, + var color: Color, + var theme: Theme, + var dateFormatter: LocalDateFormatter +) : Component { + + var padding = 5.0 + var backgroundColor = Color(0xFFFFFF) + var squareSpacing = 1.0 + var series = listOf() + var scrollPosition = 0 + private var squareSize = 0.0 + + override fun draw(canvas: Canvas) { + val width = canvas.getWidth() + val height = canvas.getHeight() + canvas.setColor(backgroundColor) + canvas.fillRect(0.0, 0.0, width, height) + squareSize = round((height - 2 * padding) / 8.0) + canvas.setFontSize(height * 0.06) + + val nColumns = floor((width - 2 * padding) / squareSize).toInt() - 2 + val todayWeekday = today.dayOfWeek + val topLeftOffset = (nColumns - 1 + scrollPosition) * 7 + todayWeekday.index + val topLeftDate = today.minus(topLeftOffset) + + repeat(nColumns) { column -> + val topOffset = topLeftOffset - 7 * column + val topDate = topLeftDate.plus(7 * column) + drawColumn(canvas, column, topDate, topOffset) + } + + canvas.setColor(theme.mediumContrastTextColor) + repeat(7) { row -> + val date = topLeftDate.plus(row) + canvas.setTextAlign(TextAlign.LEFT) + canvas.drawText( + dateFormatter.shortWeekdayName(date), + padding + nColumns * squareSize + padding, + padding + squareSize * (row + 1) + squareSize / 2 + ) + } + } + + private fun drawColumn( + canvas: Canvas, + column: Int, + topDate: LocalDate, + topOffset: Int + ) { + drawHeader(canvas, column, topDate) + repeat(7) { row -> + val offset = topOffset - row + val date = topDate.plus(row) + if (offset < 0) return + drawSquare( + canvas, + padding + column * squareSize, + padding + (row + 1) * squareSize, + squareSize - squareSpacing, + squareSize - squareSpacing, + date, + offset + ) + } + } + + private fun drawHeader(canvas: Canvas, column: Int, date: LocalDate) { + if (date.day >= 8) return + + canvas.setColor(theme.mediumContrastTextColor) + if (date.month == 1) { + canvas.drawText( + date.year.toString(), + padding + column * squareSize + squareSize / 2, + padding + squareSize / 2 + ) + } else { + canvas.drawText( + dateFormatter.shortMonthName(date), + padding + column * squareSize + squareSize / 2, + padding + squareSize / 2 + ) + } + } + + private fun drawSquare( + canvas: Canvas, + x: Double, + y: Double, + width: Double, + height: Double, + date: LocalDate, + offset: Int + ) { + + var value = if (offset >= series.size) 0.0 else series[offset] + value = round(value * 5.0) / 5.0 + + var squareColor = color.blendWith(backgroundColor, 1 - value) + var textColor = backgroundColor + + if (value == 0.0) squareColor = theme.lowContrastTextColor + if (squareColor.luminosity > 0.8) + textColor = squareColor.blendWith(theme.highContrastTextColor, 0.5) + + canvas.setColor(squareColor) + canvas.fillRect(x, y, width, height) + canvas.setColor(textColor) + canvas.drawText(date.day.toString(), x + width / 2, y + width / 2) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CheckmarkButton.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CheckmarkButton.kt new file mode 100644 index 000000000..3de5d4bb1 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/CheckmarkButton.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.core.ui.views + +import org.isoron.platform.gui.Canvas +import org.isoron.platform.gui.Color +import org.isoron.platform.gui.Component +import org.isoron.platform.gui.Font +import org.isoron.platform.gui.FontAwesome + +class CheckmarkButton( + private val value: Int, + private val color: Color, + private val theme: Theme +) : Component { + override fun draw(canvas: Canvas) { + canvas.setFont(Font.FONT_AWESOME) + canvas.setFontSize(theme.smallTextSize * 1.5) + canvas.setColor( + when (value) { + 2 -> color + else -> theme.lowContrastTextColor + } + ) + val text = when (value) { + 0 -> FontAwesome.TIMES + else -> FontAwesome.CHECK + } + canvas.drawText(text, canvas.getWidth() / 2.0, canvas.getHeight() / 2.0) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/HabitListHeader.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/HabitListHeader.kt new file mode 100644 index 000000000..703c86a6d --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/HabitListHeader.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.core.ui.views + +import org.isoron.platform.gui.Canvas +import org.isoron.platform.gui.Component +import org.isoron.platform.gui.Font +import org.isoron.platform.time.LocalDate +import org.isoron.platform.time.LocalDateFormatter + +class HabitListHeader( + private val today: LocalDate, + private val nButtons: Int, + private val theme: Theme, + private val fmt: LocalDateFormatter +) : Component { + + override fun draw(canvas: Canvas) { + val width = canvas.getWidth() + val height = canvas.getHeight() + val buttonSize = theme.checkmarkButtonSize + canvas.setColor(theme.headerBackgroundColor) + canvas.fillRect(0.0, 0.0, width, height) + + canvas.setColor(theme.headerBorderColor) + canvas.setStrokeWidth(0.5) + canvas.drawLine(0.0, height - 0.5, width, height - 0.5) + + canvas.setColor(theme.headerTextColor) + canvas.setFont(Font.BOLD) + canvas.setFontSize(theme.smallTextSize) + + repeat(nButtons) { index -> + val date = today.minus(nButtons - index - 1) + val name = fmt.shortWeekdayName(date).toUpperCase() + val number = date.day.toString() + + val x = width - (index + 1) * buttonSize + buttonSize / 2 + val y = height / 2 + canvas.drawText(name, x, y - theme.smallTextSize * 0.6) + canvas.drawText(number, x, y + theme.smallTextSize * 0.6) + } + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/NumberButton.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/NumberButton.kt new file mode 100644 index 000000000..d991a79bd --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/NumberButton.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.core.ui.views + +import org.isoron.platform.gui.Canvas +import org.isoron.platform.gui.Color +import org.isoron.platform.gui.Component +import org.isoron.platform.gui.Font +import java.lang.String.format +import kotlin.math.round + +fun Double.toShortString(): String = when { + this >= 1e9 -> format("%.1fG", this / 1e9) + this >= 1e8 -> format("%.0fM", this / 1e6) + this >= 1e7 -> format("%.1fM", this / 1e6) + this >= 1e6 -> format("%.1fM", this / 1e6) + this >= 1e5 -> format("%.0fk", this / 1e3) + this >= 1e4 -> format("%.1fk", this / 1e3) + this >= 1e3 -> format("%.1fk", this / 1e3) + this >= 1e2 -> format("%.0f", this) + this >= 1e1 -> when { + round(this) == this -> format("%.0f", this) + else -> format("%.1f", this) + } + else -> when { + round(this) == this -> format("%.0f", this) + round(this * 10) == this * 10 -> format("%.1f", this) + else -> format("%.2f", this) + } +} + +class NumberButton( + val color: Color, + val value: Double, + val threshold: Double, + val units: String, + val theme: Theme +) : Component { + + override fun draw(canvas: Canvas) { + val width = canvas.getWidth() + val height = canvas.getHeight() + val em = theme.smallTextSize + + canvas.setColor( + when { + value >= threshold -> color + value >= 0.01 -> theme.mediumContrastTextColor + else -> theme.lowContrastTextColor + } + ) + + canvas.setFontSize(theme.regularTextSize) + canvas.setFont(Font.BOLD) + canvas.drawText(value.toShortString(), width / 2, height / 2 - 0.6 * em) + + canvas.setFontSize(theme.smallTextSize) + canvas.setFont(Font.REGULAR) + canvas.drawText(units, width / 2, height / 2 + 0.6 * em) + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/Ring.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/Ring.kt new file mode 100644 index 000000000..285f09139 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/Ring.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.core.ui.views + +import org.isoron.platform.gui.Canvas +import org.isoron.platform.gui.Color +import org.isoron.platform.gui.Component +import java.lang.String.format +import kotlin.math.max +import kotlin.math.min + +class Ring( + val color: Color, + val percentage: Double, + val thickness: Double, + val radius: Double, + val theme: Theme, + val label: Boolean = false +) : Component { + + override fun draw(canvas: Canvas) { + val width = canvas.getWidth() + val height = canvas.getHeight() + val angle = 360.0 * max(0.0, min(360.0, percentage)) + + canvas.setColor(theme.lowContrastTextColor) + canvas.fillCircle(width / 2, height / 2, radius) + + canvas.setColor(color) + canvas.fillArc(width / 2, height / 2, radius, 90.0, -angle) + + canvas.setColor(theme.cardBackgroundColor) + canvas.fillCircle(width / 2, height / 2, radius - thickness) + + if (label) { + canvas.setColor(color) + canvas.setFontSize(radius * 0.4) + canvas.drawText(format("%.0f%%", percentage * 100), width / 2, height / 2) + } + } +} diff --git a/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/Themes.kt b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/Themes.kt new file mode 100644 index 000000000..449beeba0 --- /dev/null +++ b/android/uhabits-core/src/main/java/org/isoron/uhabits/core/ui/views/Themes.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.core.ui.views + +import org.isoron.platform.gui.Color + +abstract class Theme { + val toolbarColor = Color(0xffffff) + + val lowContrastTextColor = Color(0xe0e0e0) + val mediumContrastTextColor = Color(0x9E9E9E) + val highContrastTextColor = Color(0x202020) + + val cardBackgroundColor = Color(0xFFFFFF) + val appBackgroundColor = Color(0xf4f4f4) + val toolbarBackgroundColor = Color(0xf4f4f4) + val statusBarBackgroundColor = Color(0x333333) + + val headerBackgroundColor = Color(0xeeeeee) + val headerBorderColor = Color(0xcccccc) + val headerTextColor = mediumContrastTextColor + + val itemBackgroundColor = Color(0xffffff) + + fun color(paletteIndex: Int): Color { + return when (paletteIndex) { + 0 -> Color(0xD32F2F) + 1 -> Color(0x512DA8) + 2 -> Color(0xF57C00) + 3 -> Color(0xFF8F00) + 4 -> Color(0xF9A825) + 5 -> Color(0xAFB42B) + 6 -> Color(0x7CB342) + 7 -> Color(0x388E3C) + 8 -> Color(0x00897B) + 9 -> Color(0x00ACC1) + 10 -> Color(0x039BE5) + 11 -> Color(0x1976D2) + 12 -> Color(0x303F9F) + 13 -> Color(0x5E35B1) + 14 -> Color(0x8E24AA) + 15 -> Color(0xD81B60) + 16 -> Color(0x5D4037) + else -> Color(0x000000) + } + } + + val checkmarkButtonSize = 48.0 + val smallTextSize = 10.0 + val regularTextSize = 17.0 +} + +class LightTheme : Theme() diff --git a/android/uhabits-core/src/test/java/org/isoron/platform/gui/JavaCanvasTest.kt b/android/uhabits-core/src/test/java/org/isoron/platform/gui/JavaCanvasTest.kt new file mode 100644 index 000000000..a7eb11e69 --- /dev/null +++ b/android/uhabits-core/src/test/java/org/isoron/platform/gui/JavaCanvasTest.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016-2020 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.platform.gui + +import junit.framework.Assert.fail +import kotlinx.coroutines.runBlocking +import org.isoron.platform.io.JavaFileOpener +import org.junit.Test +import java.awt.image.BufferedImage +import java.awt.image.BufferedImage.TYPE_INT_ARGB + +class JavaCanvasTest { + @Test + fun run() = runBlocking { + assertRenders("components/CanvasTest.png", createCanvas(500, 400).apply { drawTestImage() }) + } +} + +fun createCanvas(w: Int, h: Int) = JavaCanvas(BufferedImage(2 * w, 2 * h, TYPE_INT_ARGB), 2.0) + +suspend fun assertRenders( + path: String, + canvas: Canvas +) { + val actualImage = canvas.toImage() + val failedActualPath = "/tmp/failed/$path" + val failedExpectedPath = failedActualPath.replace( + ".png", + ".expected.png" + ) + val failedDiffPath = failedActualPath.replace(".png", ".diff.png") + val fileOpener = JavaFileOpener() + val expectedFile = fileOpener.openResourceFile(path) + if (expectedFile.exists()) { + val expectedImage = expectedFile.toImage() + val diffImage = expectedFile.toImage() + diffImage.diff(actualImage) + val distance = diffImage.averageLuminosity * 100 + if (distance >= 1.0) { + expectedImage.export(failedExpectedPath) + actualImage.export(failedActualPath) + diffImage.export(failedDiffPath) + fail("Images differ (distance=$distance)") + } + } else { + actualImage.export(failedActualPath) + fail("Expected image file is missing. Actual image: $failedActualPath") + } +} + +suspend fun assertRenders( + width: Int, + height: Int, + expectedPath: String, + component: Component, +) { + val canvas = createCanvas(width, height) + component.draw(canvas) + assertRenders(expectedPath, canvas) +} diff --git a/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/views/BarChartTest.kt b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/views/BarChartTest.kt new file mode 100644 index 000000000..94e209070 --- /dev/null +++ b/android/uhabits-core/src/test/java/org/isoron/uhabits/core/ui/views/BarChartTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.components + +import kotlinx.coroutines.runBlocking +import org.isoron.platform.gui.assertRenders +import org.isoron.platform.time.JavaLocalDateFormatter +import org.isoron.platform.time.LocalDate +import org.isoron.uhabits.core.ui.views.BarChart +import org.isoron.uhabits.core.ui.views.LightTheme +import org.junit.Test +import java.util.Locale + +class BarChartTest { + val base = "components/BarChart" + val today = LocalDate(2015, 1, 25) + val fmt = JavaLocalDateFormatter(Locale.US) + val theme = LightTheme() + val component = BarChart(theme, fmt) + val axis = (0..100).map { today.minus(it) } + val series1 = listOf(200.0, 0.0, 150.0, 137.0, 0.0, 0.0, 500.0, 30.0, 100.0, 0.0, 300.0) + + @Test + fun testDraw() = runBlocking { + component.axis = axis + component.series.add(series1) + component.colors.add(theme.color(8)) + assertRenders(300, 200, "$base/base.png", component) + } +}