After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
~
|
||||||
|
~ 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 <http://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical" android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<org.isoron.platform.gui.AndroidCanvasTestView
|
||||||
|
android:layout_width="500dp"
|
||||||
|
android:layout_height="400dp" />
|
||||||
|
</LinearLayout>
|
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 35 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 350 B |
After Width: | Height: | Size: 345 B |
After Width: | Height: | Size: 424 B |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 3.5 KiB |
@ -0,0 +1,2 @@
|
|||||||
|
Hello World!
|
||||||
|
This is a resource.
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.platform.gui
|
||||||
|
|
||||||
|
interface Component {
|
||||||
|
fun draw(canvas: Canvas)
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.isoron.platform.gui
|
||||||
|
|
||||||
|
class FontAwesome {
|
||||||
|
companion object {
|
||||||
|
val CHECK = "\uf00c"
|
||||||
|
val TIMES = "\uf00d"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<java.awt.Font> {
|
||||||
|
val file = JavaFileOpener().openResourceFile(path) as JavaResourceFile
|
||||||
|
if (!file.exists()) throw RuntimeException("File not found: ${file.path}")
|
||||||
|
java.awt.Font.createFont(0, file.stream())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<String>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the file exists.
|
||||||
|
*/
|
||||||
|
suspend fun exists(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads resource file as an image.
|
||||||
|
*/
|
||||||
|
suspend fun toImage(): Image
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<List<Double>>()
|
||||||
|
var colors = mutableListOf<Color>()
|
||||||
|
var axis = listOf<LocalDate>()
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Double>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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()
|
@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2020 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
|
||||||
|
*
|
||||||
|
* 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|