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)
|
||||
}
|
||||
}
|