Skip to content

Commit f59e0b2

Browse files
committed
Add TileMapControlView
1 parent b45a8c9 commit f59e0b2

File tree

2 files changed

+465
-84
lines changed

2 files changed

+465
-84
lines changed
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
//
2+
// TileMapControl.swift
3+
// GateEngine
4+
//
5+
// Created by Dustin Collins on 5/1/25.
6+
//
7+
8+
public enum TileMapControlState: Int, CaseIterable {
9+
case regular = 0
10+
case highlighted = 1
11+
case selected = 2
12+
case disabled = 3
13+
}
14+
15+
public enum TileMapControlType {
16+
/// The control automatically deselects upon selection
17+
case momentary
18+
/// Only 1 element can be selected within the control
19+
case segmented
20+
/// Any element can be selected within the control
21+
case toggleable
22+
}
23+
24+
public enum TileMapControlSubControlType: Sendable {
25+
case decorative
26+
case interactable
27+
}
28+
29+
public protocol TileControlSubControl: Equatable, Identifiable {
30+
var type: TileMapControlSubControlType { get }
31+
/// The arangement of coordinates (relative to zero) for this control.
32+
/// The position of this control is determined by TileMapControlScheme
33+
var coordinates: [TileMap.Layer.Coordinate] { get }
34+
/// The `regular` state tile for the layer
35+
func regularStateTile(at coordinateIndex: Int, forLayer layer: String?) -> TileMap.Tile
36+
}
37+
38+
public protocol TileControl: Equatable, Identifiable where ID == String {
39+
associatedtype SubControl: TileControlSubControl
40+
/// How this control behaves
41+
var type: TileMapControlType { get }
42+
var subControls: [SubControl] { get }
43+
}
44+
45+
public protocol TileMapControlScheme {
46+
associatedtype Mode = AnyHashable
47+
static var defaultMode: Mode { get }
48+
static func control(at coordinate: TileMap.Layer.Coordinate, forMode mode: Mode) -> (any TileControl)?
49+
}
50+
51+
@MainActor
52+
public protocol TileMapControlViewDelegate<ControlScheme>: AnyObject {
53+
associatedtype ControlScheme: TileMapControlScheme
54+
func tileMapControlView(_ tileMapControl: TileMapControlView<ControlScheme>, currentStateforControl control: any TileControl, subControlIndex: Int) -> TileMapControlState
55+
func tileMapControlView(_ tileMapControl: TileMapControlView<ControlScheme>, control: any TileControl, subControlIndex: Int, didChangeStateTo state: TileMapControlState)
56+
func tileMapControlView(_ tileMapControl: TileMapControlView<ControlScheme>, didActivateControl control: any TileControl, subControlIndex: Int)
57+
}
58+
59+
public final class TileMapControlView<ControlScheme: TileMapControlScheme>: TileMapView {
60+
private func baseOffset(forState state: TileMapControlState) -> Int {
61+
let count = tileSet.tiles.count / type(of: state).allCases.count
62+
return state.rawValue * count
63+
}
64+
65+
var modeDidChange: Bool = true
66+
public var mode: ControlScheme.Mode = ControlScheme.defaultMode {
67+
didSet {self.modeDidChange = true}
68+
}
69+
public weak var controlDelegate: (any TileMapControlViewDelegate<ControlScheme>)? = nil
70+
71+
var controls: [any TileControl] = []
72+
var controlOrigins: [TileMap.Layer.Coordinate] = []
73+
var controlIndicies: [Int?] = []
74+
var controlStates: [[TileMapControlState]] = []
75+
76+
private func rebuildForCurrentMode() {
77+
self.controlIndicies = Array(repeating: nil, count: layers[0].columns * layers[0].rows)
78+
self.controls.removeAll(keepingCapacity: true)
79+
self.controlOrigins.removeAll(keepingCapacity: true)
80+
self.controlStates.removeAll(keepingCapacity: true)
81+
82+
guard let layer = tileMap.layers.first else {return}
83+
84+
for column in 0 ..< layer.columns {
85+
for row in 0 ..< layer.rows {
86+
let origin = TileMap.Layer.Coordinate(column: column, row: row)
87+
if let control = ControlScheme.control(at: origin, forMode: mode) {
88+
let controlIndex = controls.endIndex
89+
self.controls.append(control)
90+
self.controlOrigins.append(origin)
91+
var states: [TileMapControlState] = []
92+
var controlOriginIsASubControl = false
93+
for subControlIndex in control.subControls.indices {
94+
let subControl = control.subControls[subControlIndex]
95+
states.append(controlDelegate?.tileMapControlView(self, currentStateforControl: control, subControlIndex: subControlIndex) ?? .regular)
96+
for coordIndex in subControl.coordinates.indices {
97+
let coord = subControl.coordinates[coordIndex]
98+
if coord == .init(column: 0, row: 0) {
99+
controlOriginIsASubControl = true
100+
}
101+
let coordIndex: Int = self.coordIndex(of: origin + coord)
102+
assert(controlIndicies[coordIndex] == nil, "TileControl \(control.id) overlaps an existing control.")
103+
controlIndicies[coordIndex] = controlIndex
104+
}
105+
}
106+
assert(controlOriginIsASubControl, "A TileControl origin must be a sub control of that control.")
107+
self.controlStates.append(states)
108+
self.repaintControl(at: .init(column: column, row: row))
109+
}
110+
}
111+
}
112+
}
113+
114+
private func coordIndex(of coord: TileMap.Layer.Coordinate) -> Int {
115+
let width = Int(tileMap.size.width)
116+
return (coord.row * width) + coord.column
117+
}
118+
119+
private func coord(of coordIndex: Int) -> TileMap.Layer.Coordinate {
120+
let width = Int(tileMap.size.width)
121+
let row = coordIndex / width
122+
let column = coordIndex % width
123+
return .init(column: column, row: row)
124+
}
125+
126+
func control(at coord: TileMap.Layer.Coordinate) -> (control: any TileControl, subControlIndex: Int)? {
127+
let coordIndex = coordIndex(of: coord)
128+
if let controlIndex = controlIndicies[coordIndex] {
129+
let control = controls[controlIndex]
130+
let offset = controlOrigins[controlIndex]
131+
for subControlIndex in control.subControls.indices {
132+
let subControl = control.subControls[subControlIndex]
133+
for coordIndex in subControl.coordinates.indices {
134+
let subControlCoord = subControl.coordinates[coordIndex] + offset
135+
if subControlCoord == coord {
136+
return (control, subControlIndex)
137+
}
138+
}
139+
}
140+
}
141+
return nil
142+
}
143+
144+
func state(forControl control: any TileControl, subControlIndex: Int) -> TileMapControlState {
145+
let controlIndex = self.controls.firstIndex(where: {$0.id == control.id})!
146+
let coordIndex = controlIndicies.first(where: {$0 == controlIndex})!!
147+
return controlStates[coordIndex][subControlIndex]
148+
}
149+
func setState(_ state: TileMapControlState, forControl control: any TileControl, subControlIndex: Int) {
150+
let controlIndex = self.controls.firstIndex(where: {$0.id == control.id})!
151+
let coordIndex = controlIndicies.firstIndex(where: {$0 == controlIndex})!
152+
self.controlStates[controlIndicies[coordIndex]!][subControlIndex] = state
153+
let coord = coord(of: coordIndex)
154+
self.repaintControl(at: coord)
155+
}
156+
157+
func repaintControl(at coord: TileMap.Layer.Coordinate) {
158+
guard let controlIndex = controlIndicies[coordIndex(of: coord)] else {return}
159+
let control = controls[controlIndex]
160+
let offset = controlOrigins[controlIndex]
161+
for layer in tileMap.layers {
162+
for subControlIndex in control.subControls.indices {
163+
let subControl = control.subControls[subControlIndex]
164+
var state = self.state(forControl: control, subControlIndex: subControlIndex)
165+
self.editLayer(named: layer.name!) { layer in
166+
for coordIndex in subControl.coordinates.indices {
167+
let coord = subControl.coordinates[coordIndex] + offset
168+
let tile = subControl.regularStateTile(at: coordIndex, forLayer: layer.name)
169+
if subControl.type != .decorative && state != .disabled && state != .selected {
170+
if let tempHighlighted = hid.activeHover {
171+
if subControl.coordinates.contains(where: {$0 + offset == tempHighlighted}) {
172+
state = .highlighted
173+
}
174+
}
175+
if let tempSelected = hid.activeSelect {
176+
if subControl.coordinates.contains(where: {$0 + offset == tempSelected}) {
177+
state = .selected
178+
}
179+
}
180+
}
181+
let offset = baseOffset(forState: state)
182+
layer.setTile(.id(tile.id + offset, tile.options), at: coord)
183+
}
184+
}
185+
}
186+
}
187+
}
188+
189+
public override func update(withTimePassed deltaTime: Float) {
190+
super.update(withTimePassed: deltaTime)
191+
guard isReady else {return}
192+
if self.modeDidChange {
193+
self.modeDidChange = false
194+
self.rebuildForCurrentMode()
195+
}
196+
self.updateHID(withTimePassed: deltaTime)
197+
self.updateMomentaryToDeactivate(withTimePassed: deltaTime)
198+
}
199+
200+
public override func canBeHit() -> Bool {
201+
return true
202+
}
203+
204+
public override func didLoadLayers() {
205+
super.didLoadLayers()
206+
207+
}
208+
209+
var momentaryToDeactivate: [(pair: (control: any TileControl, subControlIndex: Int), duration: Float)] = []
210+
func updateMomentaryToDeactivate(withTimePassed deltaTime: Float) {
211+
for index in momentaryToDeactivate.indices.reversed() {
212+
momentaryToDeactivate[index].duration -= deltaTime
213+
let momentary = momentaryToDeactivate[index]
214+
if momentary.duration < 0 {
215+
self.momentaryToDeactivate.remove(at: index)
216+
self.setState(.regular, forControl: momentary.pair.control, subControlIndex: momentary.pair.subControlIndex)
217+
self.controlDelegate?.tileMapControlView(self, control: momentary.pair.control, subControlIndex: momentary.pair.subControlIndex, didChangeStateTo: .regular)
218+
}
219+
}
220+
}
221+
222+
private func didActiveControl(at coord: TileMap.Layer.Coordinate) {
223+
guard let pair = control(at: coord) else {return}
224+
guard pair.control.subControls[pair.subControlIndex].type != .decorative else {return}
225+
226+
switch pair.control.type {
227+
case .momentary:
228+
let currentState = self.state(forControl: pair.control, subControlIndex: pair.subControlIndex)
229+
guard currentState != .selected && currentState != .disabled else { return }
230+
self.setState(.selected, forControl: pair.control, subControlIndex: pair.subControlIndex)
231+
self.momentaryToDeactivate.append((pair, 0.03))
232+
self.controlDelegate?.tileMapControlView(self, control: pair.control, subControlIndex: pair.subControlIndex, didChangeStateTo: .selected)
233+
case .segmented:
234+
let currentState = self.state(forControl: pair.control, subControlIndex: pair.subControlIndex)
235+
guard currentState != .selected && currentState != .disabled else { return }
236+
for subControlIndex in pair.control.subControls.indices {
237+
let state: TileMapControlState = if subControlIndex == pair.subControlIndex {
238+
.selected
239+
}else{
240+
.regular
241+
}
242+
self.setState(state, forControl: pair.control, subControlIndex: subControlIndex)
243+
if self.state(forControl: pair.control, subControlIndex: subControlIndex) != state {
244+
self.controlDelegate?.tileMapControlView(self, control: pair.control, subControlIndex: subControlIndex, didChangeStateTo: state)
245+
}
246+
}
247+
case .toggleable:
248+
let currentState = self.state(forControl: pair.control, subControlIndex: pair.subControlIndex)
249+
guard currentState != .selected && currentState != .disabled else { return }
250+
let state: TileMapControlState = if currentState == .selected {
251+
.regular
252+
}else{
253+
.selected
254+
}
255+
self.setState(state, forControl: pair.control, subControlIndex: pair.subControlIndex)
256+
self.controlDelegate?.tileMapControlView(self, control: pair.control, subControlIndex: pair.subControlIndex, didChangeStateTo: state)
257+
}
258+
259+
self.controlDelegate?.tileMapControlView(self, didActivateControl: pair.control, subControlIndex: pair.subControlIndex)
260+
}
261+
262+
263+
264+
private var _hidOld: HIDState = .init()
265+
private var hid: HIDState = .init()
266+
267+
func updateHID(withTimePassed deltaTime: Float) {
268+
guard hid != _hidOld else {return}
269+
defer { self._hidOld = self.hid }
270+
271+
if let oldHighlight = self._hidOld.activeHover {
272+
self.repaintControl(at: oldHighlight)
273+
}
274+
if let highlight = self.hid.activeHover {
275+
self.repaintControl(at: highlight)
276+
}
277+
if let oldSelect = self._hidOld.activeSelect {
278+
self.repaintControl(at: oldSelect)
279+
}
280+
if let select = self.hid.activeSelect {
281+
self.repaintControl(at: select)
282+
}
283+
}
284+
285+
struct HIDState: Equatable {
286+
var activeHover: TileMap.Layer.Coordinate? = nil
287+
var activeSelect: TileMap.Layer.Coordinate? = nil
288+
289+
}
290+
func hid_coordFromPosition(_ p: Position2) -> TileMap.Layer.Coordinate? {
291+
let x = Int(p.x / (self.frame.width / layers.first!.size.width))
292+
let y = Int(p.y / (self.frame.height / layers.first!.size.height))
293+
return .init(column: x, row: y)
294+
}
295+
func hid_clear() {
296+
self.hid = .init()
297+
}
298+
func hid_moved(at p: Position2) {
299+
self.hid.activeHover = self.hid_coordFromPosition(p)
300+
}
301+
func hid_beginAction(at p: Position2) {
302+
self.hid.activeSelect = self.hid_coordFromPosition(p)
303+
}
304+
305+
func hid_endAction(at p: Position2) {
306+
defer {
307+
self.hid.activeSelect = nil
308+
}
309+
guard let selected = self.hid_coordFromPosition(p) else {return}
310+
guard let selecting = self.hid.activeSelect, selecting == selected else {return}
311+
self.didActiveControl(at: selected)
312+
}
313+
314+
func hid_cancel() {
315+
self.hid_clear()
316+
}
317+
318+
public override func cursorMoved(_ cursor: Mouse) {
319+
super.cursorMoved(cursor)
320+
guard let cursorPosition = cursor.locationInView(self) else {return}
321+
self.hid_moved(at: cursorPosition)
322+
}
323+
324+
public override func cursorExited(_ cursor: Mouse) {
325+
super.cursorExited(cursor)
326+
self.hid_cancel()
327+
}
328+
329+
public override func cursorButtonDown(button: MouseButton, mouse: Mouse) {
330+
super.cursorButtonDown(button: button, mouse: mouse)
331+
guard button == .primary else {return}
332+
guard let cursorPosition = mouse.locationInView(self) else {return}
333+
self.hid_beginAction(at: cursorPosition)
334+
}
335+
336+
public override func cursorButtonUp(button: MouseButton, mouse: Mouse) {
337+
super.cursorButtonUp(button: button, mouse: mouse)
338+
guard button == .primary else {return}
339+
guard let cursorPosition = mouse.locationInView(self) else {return}
340+
self.hid_endAction(at: cursorPosition)
341+
}
342+
343+
var activeTouch: Touch? = nil
344+
public override func touchesBegan(_ touches: Set<Touch>) {
345+
super.touchesBegan(touches)
346+
guard self.activeTouch == nil else {return}
347+
guard let touch = touches.first else {return}
348+
self.activeTouch = touch
349+
350+
self.hid_beginAction(at: touch.locationInView(self))
351+
}
352+
353+
public override func touchesMoved(_ touches: Set<Touch>) {
354+
super.touchesMoved(touches)
355+
guard let touch = touches.first(where: {$0 == activeTouch}) else {return}
356+
357+
self.hid_moved(at: touch.locationInView(self))
358+
}
359+
360+
public override func touchesEnded(_ touches: Set<Touch>) {
361+
super.touchesEnded(touches)
362+
guard let touch = touches.first(where: {$0 == activeTouch}) else {return}
363+
364+
self.hid_endAction(at: touch.locationInView(self))
365+
self.activeTouch = nil
366+
}
367+
368+
public override func touchesCanceled(_ touches: Set<Touch>) {
369+
super.touchesCanceled(touches)
370+
guard touches.first(where: {$0 == activeTouch}) != nil else {return}
371+
self.activeTouch = nil
372+
self.hid_cancel()
373+
}
374+
}

0 commit comments

Comments
 (0)