mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-04-05 00:37:09 +02:00
305 lines
10 KiB
Swift
305 lines
10 KiB
Swift
//
|
|
// CanvasViewController.swift
|
|
// Memola
|
|
//
|
|
// Created by Dscyre Scotti on 5/4/24.
|
|
//
|
|
|
|
import Combine
|
|
import SwiftUI
|
|
import MetalKit
|
|
import Foundation
|
|
|
|
class CanvasViewController: UIViewController {
|
|
let drawingView: DrawingView
|
|
let scrollView: UIScrollView = UIScrollView()
|
|
var renderView: MTKView {
|
|
drawingView.renderView
|
|
}
|
|
|
|
let tool: Tool
|
|
let canvas: Canvas
|
|
let history: History
|
|
let renderer: Renderer
|
|
|
|
var cancellables: Set<AnyCancellable> = []
|
|
|
|
init(tool: Tool, canvas: Canvas, history: History) {
|
|
self.tool = tool
|
|
self.canvas = canvas
|
|
self.history = history
|
|
self.drawingView = DrawingView(tool: tool, canvas: canvas, history: history)
|
|
self.renderer = Renderer(canvasView: drawingView.renderView)
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
configureViews()
|
|
configureListeners()
|
|
|
|
loadMemo()
|
|
}
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
resizeDocumentView()
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
drawingView.disableUserInteraction()
|
|
drawingView.updateDrawableSize(with: view.frame.size)
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
|
|
override func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
history.resetRedo()
|
|
Persistence.performe { context in
|
|
context.refresh(canvas, mergeChanges: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func configureViews() {
|
|
view.backgroundColor = .white
|
|
renderView.autoResizeDrawable = false
|
|
renderView.enableSetNeedsDisplay = true
|
|
renderView.translatesAutoresizingMaskIntoConstraints = false
|
|
renderView.clearColor = MTLClearColor(red: 1, green: 1, blue: 1, alpha: 1)
|
|
view.addSubview(renderView)
|
|
NSLayoutConstraint.activate([
|
|
renderView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
renderView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
renderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
renderView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
])
|
|
|
|
scrollView.maximumZoomScale = canvas.maximumZoomScale
|
|
scrollView.minimumZoomScale = canvas.minimumZoomScale
|
|
scrollView.contentInsetAdjustmentBehavior = .never
|
|
scrollView.isScrollEnabled = true
|
|
scrollView.showsVerticalScrollIndicator = true
|
|
scrollView.showsHorizontalScrollIndicator = true
|
|
scrollView.delegate = self
|
|
scrollView.backgroundColor = .clear
|
|
|
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(scrollView)
|
|
NSLayoutConstraint.activate([
|
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
|
|
])
|
|
|
|
scrollView.addSubview(drawingView)
|
|
drawingView.backgroundColor = .clear
|
|
drawingView.isUserInteractionEnabled = false
|
|
}
|
|
|
|
func resizeDocumentView(to newSize: CGSize? = nil) {
|
|
scrollView.layoutIfNeeded()
|
|
|
|
let size = canvas.size
|
|
let widthScale = (newSize?.width ?? view.frame.width) / size.width
|
|
let heightScale = (newSize?.height ?? view.frame.height) / size.height
|
|
let scale = max(widthScale, heightScale)
|
|
|
|
let width = size.width * scale
|
|
let height = size.height * scale
|
|
let newFrame = CGRect(x: 0, y: 0, width: width, height: height)
|
|
drawingView.frame = newFrame
|
|
|
|
scrollView.setZoomScale(canvas.minimumZoomScale, animated: true)
|
|
centerDocumentView(to: newSize)
|
|
|
|
let offsetX = (newFrame.width * canvas.minimumZoomScale - view.frame.width) / 2
|
|
let offsetY = (newFrame.height * canvas.minimumZoomScale - view.frame.height) / 2
|
|
|
|
let point = CGPoint(x: offsetX, y: offsetY)
|
|
scrollView.setContentOffset(point, animated: true)
|
|
|
|
drawingView.updateDrawableSize(with: view.frame.size)
|
|
}
|
|
|
|
func centerDocumentView(to newSize: CGSize? = nil) {
|
|
let documentViewSize = drawingView.frame.size
|
|
let scrollViewSize = newSize ?? view.frame.size
|
|
let verticalPadding = documentViewSize.height < scrollViewSize.height ? (scrollViewSize.height - documentViewSize.height) / 2 : 0
|
|
let horizontalPadding = documentViewSize.width < scrollViewSize.width ? (scrollViewSize.width - documentViewSize.width) / 2 : 0
|
|
self.scrollView.contentInset = UIEdgeInsets(top: verticalPadding, left: horizontalPadding, bottom: verticalPadding, right: horizontalPadding)
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func configureListeners() {
|
|
canvas.$state
|
|
.sink { [weak self] state in
|
|
self?.canvasStateChanged(state)
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
tool.$selectedPen
|
|
.sink { [weak self] pen in
|
|
self?.penChanged(to: pen)
|
|
}
|
|
.store(in: &cancellables)
|
|
|
|
history.historyPublisher
|
|
.sink { [weak self] action in
|
|
switch action {
|
|
case .undo:
|
|
self?.historyUndid()
|
|
case .redo:
|
|
self?.historyRedid()
|
|
}
|
|
}
|
|
.store(in: &cancellables)
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func loadMemo() {
|
|
canvas.load()
|
|
}
|
|
|
|
func canvasStateChanged(_ state: Canvas.State) {
|
|
guard state == .loaded else { return }
|
|
renderView.delegate = self
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController: MTKViewDelegate {
|
|
func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { }
|
|
|
|
func draw(in view: MTKView) {
|
|
guard view.drawableSize != .zero else {
|
|
return
|
|
}
|
|
canvas.updateTransform(on: drawingView)
|
|
renderer.draw(in: view, on: canvas)
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController: UIScrollViewDelegate {
|
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
drawingView
|
|
}
|
|
|
|
func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
|
magnificationStarted()
|
|
}
|
|
|
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
canvas.setZoomScale(scrollView.zoomScale)
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
}
|
|
|
|
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
|
centerDocumentView()
|
|
magnificationEnded()
|
|
}
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
draggingStarted()
|
|
}
|
|
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
}
|
|
|
|
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
if !scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
|
|
scrollViewDidEndScrolling(scrollView)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
if !decelerate, scrollView.isTracking, !scrollView.isDragging, !scrollView.isDecelerating {
|
|
scrollViewDidEndScrolling(scrollView)
|
|
}
|
|
}
|
|
|
|
func scrollViewDidEndScrolling(_ scrollView: UIScrollView) {
|
|
draggingEnded()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func magnificationStarted() {
|
|
guard !renderer.updatesViewPort else { return }
|
|
drawingView.touchCancelled()
|
|
canvas.updateClipBounds(scrollView, on: drawingView)
|
|
drawingView.disableUserInteraction()
|
|
renderer.updatesViewPort = true
|
|
}
|
|
|
|
func magnificationEnded() {
|
|
renderer.updatesViewPort = false
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
|
|
func draggingStarted() {
|
|
guard !renderer.updatesViewPort else { return }
|
|
canvas.updateClipBounds(scrollView, on: drawingView)
|
|
drawingView.disableUserInteraction()
|
|
renderer.updatesViewPort = true
|
|
}
|
|
|
|
func draggingEnded() {
|
|
renderer.updatesViewPort = false
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func penChanged(to pen: Pen?) {
|
|
if let pen, let device = drawingView.renderView.device {
|
|
pen.style.loadTexture(on: device)
|
|
}
|
|
let isPenSelected = pen != nil
|
|
scrollView.isScrollEnabled = !isPenSelected
|
|
drawingView.isUserInteractionEnabled = isPenSelected
|
|
isPenSelected ? drawingView.enableUserInteraction() : drawingView.disableUserInteraction()
|
|
}
|
|
}
|
|
|
|
extension CanvasViewController {
|
|
func historyUndid() {
|
|
guard history.undo() else { return }
|
|
drawingView.disableUserInteraction()
|
|
canvas.graphicContext.undoGraphic()
|
|
renderer.redrawsGraphicRender = true
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
|
|
func historyRedid() {
|
|
guard let event = history.redo() else { return }
|
|
drawingView.disableUserInteraction()
|
|
canvas.graphicContext.redoGraphic(for: event)
|
|
renderer.redrawsGraphicRender = true
|
|
renderer.resize(on: renderView, to: renderView.drawableSize)
|
|
renderView.draw()
|
|
drawingView.enableUserInteraction()
|
|
}
|
|
}
|