Tokamak/Sources/TokamakGTK/Shapes/Shape.swift

189 lines
4.9 KiB
Swift

// Copyright 2020 Tokamak contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Created by Morten Bek Ditlevsen on 29/12/2020.
//
import CGDK
import CGTK
import Foundation
import TokamakCore
func createPath(from elements: [Path.Element], in cr: OpaquePointer) {
var current: CGPoint = .zero
var start: CGPoint = .zero
for element in elements {
switch element {
case let .move(to: p):
cairo_move_to(cr, Double(p.x), Double(p.y))
current = p
start = p
case let .line(to: p):
cairo_line_to(cr, Double(p.x), Double(p.y))
current = p
case .closeSubpath:
cairo_close_path(cr)
current = start
case let .curve(to: p, control1: c1, control2: c2):
cairo_curve_to(
cr,
Double(c1.x),
Double(c1.y),
Double(c2.x),
Double(c2.y),
Double(p.x),
Double(p.y)
)
current = p
case let .quadCurve(to: p, control: c):
let c1 = CGPoint(x: (current.x + 2 * c.x) / 3, y: (current.y + 2 * c.y) / 3)
let c2 = CGPoint(x: (p.x + 2 * c.x) / 3, y: (p.y + 2 * c.y) / 3)
cairo_curve_to(
cr,
Double(c1.x),
Double(c1.y),
Double(c2.x),
Double(c2.y),
Double(p.x),
Double(p.y)
)
current = p
}
}
}
extension _ShapeView: GTKPrimitive {
@_spi(TokamakCore)
public var renderedBody: AnyView {
AnyView(WidgetView(build: { _ in
let w = gtk_drawing_area_new()
bindAction(to: w!)
return w!
}) {})
}
func bindAction(to drawingArea: UnsafeMutablePointer<GtkWidget>) {
drawingArea.connect(signal: "draw", closure: { widget, cr in
cairo_save(cr)
let width = gtk_widget_get_allocated_width(widget)
let height = gtk_widget_get_allocated_height(widget)
let c = (style as? Color) ?? foregroundColor ?? Color.black
var color = c.resolveToCairo(in: environment)
gdk_cairo_set_source_rgba(cr, &color)
let path = shape.path(in: CGRect(
origin: .zero,
size: CGSize(
width: Double(width),
height: Double(height)
)
))
let elements: [Path.Element]
let stroke: Bool
if case let .stroked(strokedPath) = path.storage {
elements = strokedPath.path.elements
stroke = true
let style = strokedPath.style
cairo_set_line_width(cr, Double(style.lineWidth))
cairo_set_line_join(cr, style.lineJoin.cairo)
cairo_set_line_cap(cr, style.lineCap.cairo)
cairo_set_miter_limit(cr, Double(style.miterLimit))
let dash = style.dash.map(Double.init)
cairo_set_dash(cr, dash, Int32(dash.count), Double(style.dashPhase))
} else {
elements = path.elements
stroke = false
}
cairo_set_fill_rule(cr, fillStyle.cairo)
createPath(from: elements, in: cr)
// It kind of appears to be ok to reset the clip (in order to draw outside the frame)...
// This could be error prone, however, and a source of future bugs...
cairo_reset_clip(cr)
if stroke {
cairo_stroke(cr)
} else {
cairo_fill(cr)
}
cairo_restore(cr)
})
}
}
extension CGLineJoin {
var cairo: cairo_line_join_t {
switch self {
case .miter:
return cairo_line_join_t(rawValue: 0) /* CAIRO_LINE_JOIN_MITER */
case .round:
return cairo_line_join_t(rawValue: 1) /* CAIRO_LINE_JOIN_ROUND */
case .bevel:
return cairo_line_join_t(rawValue: 2) /* CAIRO_LINE_JOIN_BEVEL */
}
}
}
extension CGLineCap {
var cairo: cairo_line_cap_t {
switch self {
case .butt:
return cairo_line_cap_t(rawValue: 0) /* CAIRO_LINE_CAP_BUTT */
case .round:
return cairo_line_cap_t(rawValue: 1) /* CAIRO_LINE_CAP_ROUND */
case .square:
return cairo_line_cap_t(rawValue: 2) /* CAIRO_LINE_CAP_SQUARE */
}
}
}
extension FillStyle {
var cairo: cairo_fill_rule_t {
if isEOFilled {
return cairo_fill_rule_t(rawValue: 1) /* CAIRO_FILL_RULE_EVEN_ODD */
} else {
return cairo_fill_rule_t(rawValue: 0) /* CAIRO_FILL_RULE_WINDING */
}
}
}
extension AnyColorBox.ResolvedValue {
var cairo: GdkRGBA {
GdkRGBA(
red: Double(red),
green: Double(green),
blue: Double(blue),
alpha: Double(opacity)
)
}
}
extension Color {
func resolveToCairo(in environment: EnvironmentValues) -> GdkRGBA {
_ColorProxy(self).resolve(in: environment).cairo
}
}