diff --git a/core/src/main/ios/org/isoron/platform/gui/IosCanvas.kt b/core/src/main/ios/org/isoron/platform/gui/IosCanvas.kt index 35293a284..085021dc2 100644 --- a/core/src/main/ios/org/isoron/platform/gui/IosCanvas.kt +++ b/core/src/main/ios/org/isoron/platform/gui/IosCanvas.kt @@ -19,8 +19,11 @@ package org.isoron.platform.gui +import kotlinx.cinterop.* import platform.CoreGraphics.* +import platform.Foundation.* import platform.UIKit.* +import kotlin.math.* val Color.uicolor: UIColor get() = UIColor.colorWithRed(this.red, this.green, this.blue, this.alpha) @@ -30,9 +33,13 @@ val Color.cgcolor: CGColorRef? class IosCanvas(val width: Double, val height: Double, - val pixelScale: Double = 2.0 + val scale: Double = 2.0 ) : Canvas { + var textColor = UIColor.blackColor + var font = Font.REGULAR + var fontSize = 12.0 + var textAlign = TextAlign.CENTER val ctx = UIGraphicsGetCurrentContext()!! override fun setColor(color: Color) { @@ -42,28 +49,46 @@ class IosCanvas(val width: Double, } override fun drawLine(x1: Double, y1: Double, x2: Double, y2: Double) { - CGContextMoveToPoint(ctx, x1 * pixelScale, y1 * pixelScale) - CGContextAddLineToPoint(ctx, x2 * pixelScale, y2 * pixelScale) + CGContextMoveToPoint(ctx, x1 * scale, y1 * scale) + CGContextAddLineToPoint(ctx, x2 * scale, y2 * scale) CGContextStrokePath(ctx) } + @Suppress("CAST_NEVER_SUCCEEDS") override fun drawText(text: String, x: Double, y: Double) { + val sx = scale * x + val sy = scale * y + val nsText = (text as NSString) + val uiFont = when (font) { + Font.REGULAR -> UIFont.systemFontOfSize(fontSize) + Font.BOLD -> UIFont.boldSystemFontOfSize(fontSize) + Font.FONT_AWESOME -> UIFont.fontWithName("FontAwesome", fontSize) + } + val size = nsText.sizeWithFont(uiFont) + val width = size.useContents { width } + val height = size.useContents { height } + val origin = when (textAlign) { + TextAlign.CENTER -> CGPointMake(sx - width / 2, sy - height / 2) + TextAlign.LEFT -> CGPointMake(sx, sy - height / 2) + TextAlign.RIGHT -> CGPointMake(sx - width, sy - height / 2) + } + nsText.drawAtPoint(origin, uiFont) } override fun fillRect(x: Double, y: Double, width: Double, height: Double) { CGContextFillRect(ctx, - CGRectMake(x * pixelScale, - y * pixelScale, - width * pixelScale, - height * pixelScale)) + CGRectMake(x * scale, + y * scale, + width * scale, + height * scale)) } override fun drawRect(x: Double, y: Double, width: Double, height: Double) { CGContextStrokeRect(ctx, - CGRectMake(x * pixelScale, - y * pixelScale, - width * pixelScale, - height * pixelScale)) + CGRectMake(x * scale, + y * scale, + width * scale, + height * scale)) } override fun getHeight(): Double { @@ -75,14 +100,15 @@ class IosCanvas(val width: Double, } override fun setFont(font: Font) { + this.font = font } override fun setFontSize(size: Double) { - CGContextSetFontSize(ctx, size * pixelScale) + this.fontSize = size * scale } override fun setStrokeWidth(size: Double) { - CGContextSetLineWidth(ctx, size * pixelScale) + CGContextSetLineWidth(ctx, size * scale) } override fun fillArc(centerX: Double, @@ -90,12 +116,31 @@ class IosCanvas(val width: Double, radius: Double, startAngle: Double, swipeAngle: Double) { + val a1 = startAngle / 180 * PI * (-1) + val a2 = a1 - swipeAngle / 180 * PI + CGContextBeginPath(ctx) + CGContextMoveToPoint(ctx, centerX * scale, centerY * scale) + CGContextAddArc(ctx, + centerX * scale, + centerY * scale, + radius * scale, + a1, + a2, + if (swipeAngle > 0) 1 else 0) + CGContextClosePath(ctx) + CGContextFillPath(ctx) } override fun fillCircle(centerX: Double, centerY: Double, radius: Double) { + val rect = CGRectMake(scale * (centerX - radius), + scale * (centerY - radius), + scale * radius * 2.0, + scale * radius * 2.0) + CGContextFillEllipseInRect(ctx, rect) } override fun setTextAlign(align: TextAlign) { + this.textAlign = align } override fun toImage(): Image { diff --git a/core/src/main/ios/org/isoron/platform/io/IosDatabase.kt b/core/src/main/ios/org/isoron/platform/io/IosDatabase.kt index 2ede7f7c2..2f3501bc1 100644 --- a/core/src/main/ios/org/isoron/platform/io/IosDatabase.kt +++ b/core/src/main/ios/org/isoron/platform/io/IosDatabase.kt @@ -26,7 +26,7 @@ fun sqlite3_errstr(db: CPointer): String { return "SQLite3 error: " + sqlite3_errmsg(db).toString() } -class IosDatabaseOpener() : DatabaseOpener { +class IosDatabaseOpener : DatabaseOpener { override fun open(file: UserFile): Database = memScoped { val db = alloc>() val path = (file as IosFile).path diff --git a/core/src/test/ios/org/isoron/DependencyResolver.kt b/core/src/test/ios/org/isoron/DependencyResolver.kt index 0044336af..2626eb341 100644 --- a/core/src/test/ios/org/isoron/DependencyResolver.kt +++ b/core/src/test/ios/org/isoron/DependencyResolver.kt @@ -19,7 +19,6 @@ package org.isoron -import kotlinx.cinterop.* import org.isoron.platform.gui.* import org.isoron.platform.io.* import org.isoron.platform.time.* @@ -40,7 +39,7 @@ actual object DependencyResolver { actual fun createCanvas(width: Int, height: Int): Canvas { val scale = 2.0 UIGraphicsBeginImageContext(CGSizeMake(width * scale, height * scale)) - return IosCanvas(width * scale, height * scale, pixelScale = scale) + return IosCanvas(width * scale, height * scale, scale = scale) } actual suspend fun getDatabase(): Database { diff --git a/ios/Application/AppDelegate.swift b/ios/Application/AppDelegate.swift index d77771f14..df6fc7c0f 100644 --- a/ios/Application/AppDelegate.swift +++ b/ios/Application/AppDelegate.swift @@ -30,7 +30,7 @@ import UIKit didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { backend = Backend(databaseName: "uhabits.db", - databaseOpener: IosDatabaseOpener(withLog: log), + databaseOpener: IosDatabaseOpener(), fileOpener: IosFileOpener(), localeHelper: IosLocaleHelper(log: log), log: log, diff --git a/ios/Application/Platform/ComponentView.swift b/ios/Application/Platform/ComponentView.swift new file mode 100644 index 000000000..8b111651c --- /dev/null +++ b/ios/Application/Platform/ComponentView.swift @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2016-2019 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +import UIKit + +class ComponentView : UIView { + var component: Component? + + init(frame: CGRect, component: Component?) { + self.component = component + super.init(frame: frame) + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + + override func draw(_ rect: CGRect) { + let canvas = IosCanvas(width: Double(rect.width), + height: Double(rect.height), + scale: 1.0) + component?.draw(canvas: canvas) + } + + override func layoutSubviews() { + setNeedsDisplay() + } +} diff --git a/ios/Application/Platform/IosCanvas.swift b/ios/Application/Platform/IosCanvas.swift deleted file mode 100644 index 5a5d4a34c..000000000 --- a/ios/Application/Platform/IosCanvas.swift +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -import UIKit - -extension Color { - var uicolor: UIColor { - return UIColor(red: CGFloat(self.red), - green: CGFloat(self.green), - blue: CGFloat(self.blue), - alpha: CGFloat(self.alpha)) - } - - var cgcolor : CGColor { - return uicolor.cgColor - } -} - -class ComponentView : UIView { - var component: Component? - - init(frame: CGRect, component: Component?) { - self.component = component - super.init(frame: frame) - } - - required init?(coder aDecoder: NSCoder) { - fatalError() - } - - override func draw(_ rect: CGRect) { - let canvas = IosCanvas(withBounds: bounds) - component?.draw(canvas: canvas) - } - - override func layoutSubviews() { - setNeedsDisplay() - } -} - -class IosCanvas : NSObject, Canvas { - var bounds: CGRect - var ctx: CGContext - - var font = Font.regular - var textSize = CGFloat(12) - var textColor = UIColor.black - var textAlign = TextAlign.center - - init(withBounds bounds: CGRect) { - self.bounds = bounds - self.ctx = UIGraphicsGetCurrentContext()! - } - - func setColor(color: Color) { - self.ctx.setStrokeColor(color.cgcolor) - self.ctx.setFillColor(color.cgcolor) - textColor = color.uicolor - } - - func drawLine(x1: Double, y1: Double, x2: Double, y2: Double) { - self.ctx.move(to: CGPoint(x: CGFloat(x1), y: CGFloat(y1))) - self.ctx.addLine(to: CGPoint(x: CGFloat(x2), y: CGFloat(y2))) - self.ctx.strokePath() - - } - - func drawText(text: String, x: Double, y: Double) { - let nsText = text as NSString - - var uifont = UIFont.systemFont(ofSize: textSize) - if font == Font.bold { - uifont = UIFont.boldSystemFont(ofSize: textSize) - } - if font == Font.fontAwesome { - uifont = UIFont(name: "FontAwesome", size: textSize)! - } - - let attrs = [NSAttributedString.Key.font: uifont, - NSAttributedString.Key.foregroundColor: textColor] - - let size = nsText.size(withAttributes: attrs) - if textAlign == TextAlign.center { - nsText.draw(at: CGPoint(x: CGFloat(x) - size.width / 2, - y : CGFloat(y) - size.height / 2), - withAttributes: attrs) - } else if textAlign == TextAlign.left { - nsText.draw(at: CGPoint(x: CGFloat(x), - y : CGFloat(y) - size.height / 2), - withAttributes: attrs) - } else { - nsText.draw(at: CGPoint(x: CGFloat(x) - size.width, - y : CGFloat(y) - size.height / 2), - withAttributes: attrs) - } - } - - func drawRect(x: Double, y: Double, width: Double, height: Double) { - self.ctx.stroke(CGRect(x: CGFloat(x), - y: CGFloat(y), - width: CGFloat(width), - height: CGFloat(height))) - } - - func fillRect(x: Double, y: Double, width: Double, height: Double) { - self.ctx.fill(CGRect(x: CGFloat(x), - y: CGFloat(y), - width: CGFloat(width), - height: CGFloat(height))) - } - - func getHeight() -> Double { - return Double(bounds.height) - } - - func getWidth() -> Double { - return Double(bounds.width) - } - - func setFontSize(size: Double) { - self.textSize = CGFloat(size) - } - - func setFont(font: Font) { - self.font = font - } - - func setStrokeWidth(size: Double) { - self.ctx.setLineWidth(CGFloat(size)) - } - - func setTextAlign(align: TextAlign) { - self.textAlign = align - } - - func fillArc(centerX: Double, centerY: Double, radius: Double, startAngle: Double, swipeAngle: Double) { - let center = CGPoint(x: CGFloat(centerX), y: CGFloat(centerY)) - let a1 = startAngle / 180 * .pi * (-1) - let a2 = a1 - swipeAngle / 180 * .pi - self.ctx.beginPath() - self.ctx.move(to: center) - self.ctx.addArc(center: center, - radius: CGFloat(radius), - startAngle: CGFloat(a1), - endAngle: CGFloat(a2), - clockwise: swipeAngle >= 0) - self.ctx.closePath() - self.ctx.fillPath() - } - - func fillCircle(centerX: Double, centerY: Double, radius: Double) { - self.ctx.fillEllipse(in: CGRect(x: CGFloat(centerX - radius), - y: CGFloat(centerY - radius), - width: CGFloat(radius * 2), - height: CGFloat(radius * 2))) - } -} diff --git a/ios/Tests/Platform/IosCanvasTest.swift b/ios/Tests/Platform/IosCanvasTest.swift deleted file mode 100644 index 2ae212914..000000000 --- a/ios/Tests/Platform/IosCanvasTest.swift +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2016-2019 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -import XCTest -import UIKit - -@testable import uhabits - -class IosCanvasTest : XCTestCase { - func testDraw() { - UIGraphicsBeginImageContext(CGSize(width: 500, height: 400)) - let canvas = IosCanvas(withBounds: CGRect(x: 0, y: 0, width: 500, height: 400)) - - canvas.setColor(color: Color(rgb: 0x303030)) - canvas.fillRect(x: 0.0, y: 0.0, width: 500.0, height: 400.0) - - canvas.setColor(color: Color(rgb: 0x606060)) - canvas.setStrokeWidth(size: 25.0) - canvas.drawRect(x: 100.0, y: 100.0, width: 300.0, height: 200.0) - - canvas.setColor(color: Color(rgb: 0xFFFF00)) - canvas.setStrokeWidth(size: 1.0) - canvas.fillCircle(centerX: 50.0, centerY: 50.0, radius: 30.0) - canvas.fillArc(centerX: 50.0, centerY: 150.0, radius: 30.0, startAngle: 90.0, swipeAngle: 135.0) - canvas.fillArc(centerX: 50.0, centerY: 250.0, radius: 30.0, startAngle: 90.0, swipeAngle: -135.0) - canvas.fillArc(centerX: 50.0, centerY: 350.0, radius: 30.0, startAngle: 45.0, swipeAngle: 90.0) - canvas.drawRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0) - canvas.drawRect(x: 0.0, y: 100.0, width: 100.0, height: 100.0) - canvas.drawRect(x: 0.0, y: 200.0, width: 100.0, height: 100.0) - canvas.drawRect(x: 0.0, y: 300.0, width: 100.0, height: 100.0) - - canvas.setColor(color: Color(rgb: 0xFF0000)) - canvas.setStrokeWidth(size: 2.0) - canvas.drawLine(x1: 0.0, y1: 0.0, x2: 500.0, y2: 400.0) - canvas.drawLine(x1: 500.0, y1: 0.0, x2: 0.0, y2: 400.0) - - canvas.setFontSize(size: 50.0) - canvas.setColor(color: Color(rgb: 0x00FF00)) - canvas.drawText(text: "Test", x: 250.0, y: 200.0) - - canvas.setFont(font: Font.bold) - canvas.drawText(text: "Test", x: 250.0, y: 100.0) - - canvas.setFont(font: Font.fontAwesome) - canvas.drawText(text: FontAwesome.Companion().check, x: 250.0, y: 300.0) - - let image = UIGraphicsGetImageFromCurrentImageContext()! - let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - let filePath = paths.first?.appendingPathComponent("IosCanvasTest.png") - try! image.pngData()!.write(to: filePath!, options: .atomic) - } -} diff --git a/ios/uhabits.xcodeproj/project.pbxproj b/ios/uhabits.xcodeproj/project.pbxproj index f0ad0703c..eb0053c52 100644 --- a/ios/uhabits.xcodeproj/project.pbxproj +++ b/ios/uhabits.xcodeproj/project.pbxproj @@ -17,8 +17,7 @@ 00C0C6BF22465F65003D8AF0 /* migrations in Resources */ = {isa = PBXBuildFile; fileRef = 00C0C6BC22465F65003D8AF0 /* migrations */; }; 00C0C6CA2246E543003D8AF0 /* main.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C0C6C92246E543003D8AF0 /* main.framework */; }; 00C0C6CC2246E550003D8AF0 /* main.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 00C0C6C92246E543003D8AF0 /* main.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 00C0C6D122470705003D8AF0 /* IosCanvas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6D022470705003D8AF0 /* IosCanvas.swift */; }; - 00C0C6D92247DC13003D8AF0 /* IosCanvasTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6D82247DC13003D8AF0 /* IosCanvasTest.swift */; }; + 00C0C6D122470705003D8AF0 /* ComponentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6D022470705003D8AF0 /* ComponentView.swift */; }; 00C0C6E0224A3602003D8AF0 /* DetailScreenController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00C0C6DE224A35FC003D8AF0 /* DetailScreenController.swift */; }; 00D48BD12200A31300CC4527 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 00D48BD02200A31300CC4527 /* Launch.storyboard */; }; 00D48BD32200AC1600CC4527 /* EditHabitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00D48BD22200AC1600CC4527 /* EditHabitController.swift */; }; @@ -62,8 +61,7 @@ 00C0C6BB22465F65003D8AF0 /* databases */ = {isa = PBXFileReference; lastKnownFileType = folder; path = databases; sourceTree = ""; }; 00C0C6BC22465F65003D8AF0 /* migrations */ = {isa = PBXFileReference; lastKnownFileType = folder; path = migrations; sourceTree = ""; }; 00C0C6C92246E543003D8AF0 /* main.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = main.framework; path = ../core/build/bin/ios/mainDebugFramework/main.framework; sourceTree = ""; }; - 00C0C6D022470705003D8AF0 /* IosCanvas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosCanvas.swift; sourceTree = ""; }; - 00C0C6D82247DC13003D8AF0 /* IosCanvasTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IosCanvasTest.swift; sourceTree = ""; }; + 00C0C6D022470705003D8AF0 /* ComponentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComponentView.swift; sourceTree = ""; }; 00C0C6DE224A35FC003D8AF0 /* DetailScreenController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailScreenController.swift; sourceTree = ""; }; 00D48BD02200A31300CC4527 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = ""; }; 00D48BD22200AC1600CC4527 /* EditHabitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditHabitController.swift; sourceTree = ""; }; @@ -138,7 +136,6 @@ isa = PBXGroup; children = ( 00A5B43E22009F5A0024E00C /* Info.plist */, - 00C0C6D722472BC9003D8AF0 /* Platform */, ); path = Tests; sourceTree = ""; @@ -165,15 +162,7 @@ 00C0C6D622471BA3003D8AF0 /* Platform */ = { isa = PBXGroup; children = ( - 00C0C6D022470705003D8AF0 /* IosCanvas.swift */, - ); - path = Platform; - sourceTree = ""; - }; - 00C0C6D722472BC9003D8AF0 /* Platform */ = { - isa = PBXGroup; - children = ( - 00C0C6D82247DC13003D8AF0 /* IosCanvasTest.swift */, + 00C0C6D022470705003D8AF0 /* ComponentView.swift */, ); path = Platform; sourceTree = ""; @@ -304,7 +293,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00C0C6D122470705003D8AF0 /* IosCanvas.swift in Sources */, + 00C0C6D122470705003D8AF0 /* ComponentView.swift in Sources */, 00C0C6E0224A3602003D8AF0 /* DetailScreenController.swift in Sources */, 00A5B42A22009F590024E00C /* MainScreenController.swift in Sources */, 00A5B42822009F590024E00C /* AppDelegate.swift in Sources */, @@ -317,7 +306,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 00C0C6D92247DC13003D8AF0 /* IosCanvasTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };