Vala プログラミング

WebGPU プログラミング

おなが@京都先端科学大

GNUStepSwiftBridge & CairoGraphics

前回の GNUStepSwiftBridge を利用して、CairoGraphics を行いました。
実行結果
起動時

メニューアイテム(AItem)をクリック時

InfoPanel の表示

前回からの変更点
Resources ファイルの .build/debug ディレクトリへのコピー
 前回はResouces を手動で コピーしていましたが、これをプログラム内で行うようにしました。FileManager を使用します。
CheckResource.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book

// check Resources Files
import Foundation

func CheckResource() {
    let fileManager = FileManager.default
    let currentPath = fileManager.currentDirectoryPath
    let filePath:String =  currentPath + "/Sources/Resources"
    let copyPath:String = ".build/debug/Resources"

    let exist = fileManager.fileExists(atPath: copyPath)
    if !exist {
        do {
            try fileManager.copyItem(atPath: filePath, toPath: copyPath)
    	} catch {
            print("can not copy")
    	}
    }
}

AppKit ライブラリ
 前回はAppKit ライブラリを application project と同じフォルダに置いていましたが、これをAppKit 単体のライブラリにしました。
方法は以下の通り。

1 mkdir AppKit
2 cd AppKit
   swift package init --type library
3 copy AppKitGNUStep , FoundationGNUStep, libobjc2 , ObjCSwiftInterop in Sources dir
   copy Headers in AppKit dir
4 edit Sources/AppKit/AppKit.swift
   copy Foundation.swift , SwiftForGNUStep.swift
5 edit Package.swift
   // swift-tools-version: 5.8
   // The swift-tools-version declares the minimum version of Swift required to build
   this package.

   import PackageDescription

   let package = Package(
       name: "AppKit",
       products: [
           // Products define the executables and libraries a package produces, 
                 making them visible to other packages.
           .library(name: "libobjc", targets: ["libobjc2"]),
           .library(name: "AppKitGNUStep", targets: ["AppKitGNUStep"]),
           .library(name: "FoundationGNUStep", targets: ["FoundationGNUStep"]),
           .library(name: "ObjCSwiftInterop",  targets: ["ObjCSwiftInterop"]),
           .library(
               name: "AppKit",
               targets: ["AppKit"]),
       ],
       targets: [
           // Targets are the basic building blocks of a package,
                 defining a module or a test suite.
           // Targets can depend on other targets in this package and products
                 from dependencies.
           .target(name: "ObjCSwiftInterop"),
           .target(
                name: "AppKit",
                dependencies: ["libobjc2", "AppKitGNUStep", "ObjCSwiftInterop",
                                         "FoundationGNUStep"]),
           .systemLibrary(name: "libobjc2"),
           .systemLibrary(name: "AppKitGNUStep"),
           .systemLibrary(name: "FoundationGNUStep"),
           .testTarget(
                name: "AppKitTests",
                dependencies: ["AppKit"]),
       ]
   )
5 set git info
   in AppKit dir
       git init
       git add .
       git commit -m "Initial Commit"
       git tag 1.0.0

application (AppCG) ディレクトリ配置

    AppKit (上記ライブラリ)
    AppCG
       Sources
            CCairo
                 module.modulemap
            Resources
                 MainMenu.gorm
                 Info-gnustep.plist
                 ( tiff files )
            main.swift
            AppDelegate.swift
            Image.swift
            CGContext.swift
            CheckResource.swift
       Package.swift

MainMenu.gorm
Gorm を使って編集します。
 1 InfoPanel の表示
  メニューの InfoPanel を NSFirst に接続
  target から orderFrontStandardInfoPanel: を選択
 2 メニューアイテムの追加
  パレットウィンドウのメニューから、 Item をドラッグ

CCairo
module.modulemap

module CCairo [system] {
    module CairoXlib {
        header "/usr/include/cairo/cairo-xlib.h"
    }
    module Cairo {
        header "/usr/include/cairo/cairo.h"
    }
    link "cairo"
    export *
}

Package.swift

// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "AppCG",
    dependencies: [
        .package(url: "../AppKit", from: Version(1,0,0))
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .executableTarget(
            name: "AppCG",
            dependencies: ["AppKit", "CCairo"],
            path: "Sources"
         ),
         .systemLibrary(
            name: "CCairo"),
    ]
)

main.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book

import AppKitGNUStep
import ObjCSwiftInterop
import Foundation

// check ResourcesFile
CheckResource()

// main
let fileManager = FileManager.default

let delegate = AppDelegate()

let napClass =  objc_getClass("NSApplication")
var sharedApp =  forSwift_objcSendMessage(&napClass!.pointee, sel_registerName("sharedApplication"))
print("Just created NSApplication")

let v = forSwift_objcSendMessage1ID(&sharedApp!.pointee, sel_getUid("setDelegate:"), delegate._nsobjptr!)
print("made it!")

NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv.pointee)

AppDelegate.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book

import FoundationGNUStep
import ObjCSwiftInterop
import AppKit
import CCairo.Cairo

let width = 640
let height = 480
var drawFlag = 1

func draw(_ context: CGContext, _ width: Float, _ height: Float) -> Void {
   // background
   context.setColor(1, 1, 1, 1)
   context.addRect(CGRect(x:0, y:0, width: Double(width), height: Double(height)))
   context.fillPath()

   switch (drawFlag) {
      case 1:
      // Draw random rects (some stroked, some filled)
      for i in 0...19 {
         if (i % 2 == 0) {
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
               Double.random(in: 0...1),  Double.random(in: 0...1))
            context.addRect(CGRect(
               x:Double.random(in: 0...1)*Double(width),
               y:Double.random(in: 0...1)*Double(height),
               width:Double.random(in: 0...1)*Double(width),
               height:Double.random(in: 0...1)*Double(height)))
            context.fillPath()
         }
         else {
            context.lineWidth = Float.random(in: 0...10) + 2
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
               Double.random(in: 0...1),  Double.random(in: 0...1))
            context.addRect(CGRect(
               x:Double.random(in: 0...1)*Double(width),
               y:Double.random(in: 0...1)*Double(height),
               width:Double.random(in: 0...1)*Double(width),
               height:Double.random(in: 0...1)*Double(height)))
            context.strokePath()
         }
      }

      case 2:
      // Draw random circles (some stroked, some filled)
      for i in 0...19 {
         context.addArc(
            center: CGPoint(x:Double.random(in: 0...1)*Double(width),
                            y:Double.random(in: 0...1)*Double(height)),
            radius: Float.random(in: 0...1)*((width>height) ? Float(height) : Float(width)),
            startAngle: 0, endAngle: 2*Float.pi, clockwise: false)
      
         if (i % 2 == 0) {
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
               Double.random(in: 0...1),  Double.random(in: 0...1))
            context.fillPath()
         }
         else {
            context.lineWidth = Float.random(in: 0...10) + 2
            context.setColor( Double.random(in: 0...1), Double.random(in: 0...1),
               Double.random(in: 0...1),  Double.random(in: 0...1))
            context.strokePath()
         }
      }

      default:
         break
   }
}

func display() {
	let cs = cairo_image_surface_create( CAIRO_FORMAT_ARGB32, Int32(width), Int32(height) )
	let c = CGContext(surface: cs, width: width, height: height)

   // draw
   draw(c, Float(width), Float(height))
   cairo_surface_write_to_png( cs, "test.png" )
}

class AppDelegate: NSApplicationDelegate {
	lazy var window = NSWindow(CGRect(x: 150, y: 380, width: 500, height: 450))
	lazy var imageView = NSImageView()
	lazy var view = UIView()

	override func applicationDidFinishLaunching(notification: Any?) {
		print("\nget mainMenu")
		let menu =  forSwift_objcSendMessage(&sharedApp!.pointee, sel_registerName("mainMenu"))
		// menu: add Action
		print("item at index")
		var aindex : Int = 1
		print("item index:", aindex)
		let aindexNSInt: UnsafeMutablePointer<objc_object> = unsafeBitCast(aindex, to: UnsafeMutablePointer<objc_object>.self)
		let aitem = forSwift_objcSendMessage1ID(&menu!.pointee, sel_getUid("itemAtIndex:"), aindexNSInt)
		print("index \(aindex) item:", aitem)
		let aitem2 = objc_convertToSwift_NSObject(value: aitem) as? NSString
		let aitemtitle = forSwift_objcSendMessage(&aitem!.pointee, sel_registerName("title"))
		let aitemtitle2 = objc_convertToSwift_NSObject(value: aitemtitle) as? NSString
		print("index \(aindex) item:", aitemtitle2!.string)
		
		let my_actionIMP: @convention(block) () -> () = {
			print("print from menu")
			drawFlag = 2
            display()

			let currentPath = fileManager.currentDirectoryPath
			let filePath:String =  currentPath + "/test.png"
        	let image2 = Image.imageWithPath(filePath)
        	self.imageView.setImage(image2!)
		}
		let types = "@"
		let imp = imp_implementationWithBlock(unsafeBitCast(my_actionIMP, to: id.self))
		class_addMethod(object_getClass(&aitem!.pointee), sel_registerName("my_action:"),imp, types)
		
		_ = forSwift_objcSendMessage1ID(&aitem!.pointee, sel_registerName("setTarget:"), aitem!)
		let sel = sel_getUid("my_action:")
		_ = forSwift_objcSendMessage1SEL(&aitem!.pointee, sel_registerName("setAction:"), sel!)
		//
        print("end menu\n")
        
        display()
        
		let currentPath = fileManager.currentDirectoryPath
		let filePath:String =  currentPath + "/test.png"
    	let image = Image.imageWithPath(filePath)
        
		window.orderFront(sender: self)
		window.setTitle(NSString(string: "Image Test"))

		view.frame = .init(x: 0, y: 0, width: 500, height: 450)

		imageView.frame = .init(x: 0, y: 0, width: 500, height: 450)
		
		imageView.setImage(image!)
		
		view.addSubview(imageView)

		window.setContentView(view)
	}
}

Image.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book

import FoundationGNUStep
import ObjCSwiftInterop
import AppKit

class Image {
	static func imageWithPath(_ path: String) -> NSImage? {
		print("file path:", path)
		let nsfilePath = NSString(string: path)
		var nsImageClass = objc_getClass("NSImage")
		var initializedObject = forSwift_objcSendMessage(&nsImageClass!.pointee, sel_getUid("alloc"))
		var vimage = forSwift_objcSendMessage1(&initializedObject!.pointee, sel_getUid("initWithContentsOfFile:"), nsfilePath._nsobjptr)
        //print("vimage type:", type(of: vimage))
        let v = NSImage(nsobjptr: &vimage!.pointee)
        return v
    }
}

CGContext.swift

// The Swift Programming Language
// https://docs.swift.org/swift-book
//
//  CGContext.swift
//  CairoGraphics
//

import ObjCSwiftInterop
import CCairo.Cairo

open class CGContext {
    public var surface: Optional<OpaquePointer>
    public var context: Optional<OpaquePointer>
    
    public internal(set) var height: Int = 0
    public internal(set) var width: Int = 0

    public init(surface: Optional<OpaquePointer>, width: Int, height: Int) {
        self.context = cairo_create(surface)
        self.surface = surface
        self.width = width
        self.height = height
    }
}

public extension CGContext {
    func flush() {
        cairo_surface_flush(surface)
    }
}

public extension CGContext {
    func beginPath() {
        cairo_new_path(context)
    }
    
    func closePath() {
        cairo_close_path(context)
    }
    
    var currentPointOfPath: CGPoint {
        var x: Double = .zero
        var y: Double = .zero
        cairo_get_current_point(context, &x, &y)
        return CGPoint(x: x, y: y)
    }
    
    var boundingBoxOfPath: CGRect {
        var x1: Double = .zero
        var y1: Double = .zero
        var x2: Double = .zero
        var y2: Double = .zero
        
        cairo_path_extents(context, &x1, &y1, &x2, &y2)
        
        if x1.isZero && y1.isZero && x2.isZero && y2.isZero {
            //return .null
            return CGRect(x: x1, y: y1, width: x2, height: y2)
        } else {
            return CGRect(x: min(x1, x2), y: min(y1, y2), width: max(x1, x2) - min(x1, x2), height: max(y1, y2) - min(y1, y2))
        }
    }
}

public extension CGContext {
    func move(to point: CGPoint) {
        cairo_move_to(context, Double(point.x), Double(point.y))
    }
    
    func addLine(to point: CGPoint) {
        cairo_line_to(context, Double(point.x), Double(point.y))
    }
    
    func addRect(_ rect: CGRect) {
        cairo_rectangle(context, Double(rect.x), Double(rect.y), Double(rect.width), Double(rect.height))
    }
    
    func addCurve(to end: CGPoint, control1: CGPoint, control2: CGPoint) {
        cairo_curve_to(context,
                       Double(control1.x), Double(control1.y),
                       Double(control2.x), Double(control2.y),
                       Double(end.x), Double(end.y))
    }
    
    func addQuadCurve(to end: CGPoint, control: CGPoint) {
        let current = currentPointOfPath
        
        let control1 = CGPoint(x: (current.x / 3.0) + (2.0 * control.x / 3.0), y: (current.y / 3.0) + (2.0 * control.y / 3.0))
        let control2 = CGPoint(x: (2.0 * control.x / 3.0) + (end.x / 3.0), y: (2.0 * control.y / 3.0) + (end.y / 3.0))
        
        addCurve(to: end, control1: control1, control2: control2)
    }
    
    func addLines(between points: [CGPoint]) {
        if points.count == 0 { return }
        
        move(to: points[0])
        
        for i in 1..<points.count {
            addLine(to: points[i])
        }
    }
    
    func addRects(_ rects: [CGRect]) {
        for rect in rects {
            addRect(rect)
        }
    }
    
    func addArc(center: CGPoint, radius: Float, startAngle: Float, endAngle: Float, clockwise: Bool) {
        if clockwise {
            cairo_arc_negative(context, Double(center.x), Double(center.y), Double(radius), Double(startAngle), Double(endAngle))
        } else {
            cairo_arc(context, Double(center.x), Double(center.y), Double(radius), Double(startAngle), Double(endAngle))
        }
    }
}

public extension CGContext {
    func fillPath() {
        cairo_fill(context)
    }

    func clip() {
        cairo_clip(context)
    }
    
    func resetClip() {
        cairo_reset_clip(context)
    }
    
    func strokePath() {
        cairo_stroke(context)
    }
}

public extension CGContext {
    func fill(_ rect: CGRect) {
        beginPath()
        addRect(rect)
        closePath()
    }
    
    func stroke(_ rect: CGRect) {
        beginPath()
        addRect(rect)
        closePath()
        strokePath()
    }
}

public extension CGContext {
    func setColor(_ r:Double, _ g:Double, _ b:Double, _ a:Double) {
        cairo_set_source_rgba(context, r, g, b, a);
    }
    
    var lineWidth: Float {
        get {
            return Float(cairo_get_line_width(context))
        }
        set {
            cairo_set_line_width(context, Double(newValue))
        }
    }    
}