Skip to content

Commit defdd21

Browse files
committed
Fix retain cycles
1 parent 67494d2 commit defdd21

File tree

3 files changed

+243
-9
lines changed

3 files changed

+243
-9
lines changed

Sources/GateEngine/UI/Button.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ open class Button: Control {
5555

5656
public final func sendActions(forEvent event: Event) {
5757
if let eventActionStorage = eventActionStorage[event] {
58+
weak let unownedSelf: Button! = self
5859
for block in eventActionStorage {
59-
block(self)
60+
block(unownedSelf)
6061
}
6162
}
6263
}
@@ -147,6 +148,8 @@ open class Button: Control {
147148
public init(size: Size2? = nil, label: String? = nil, textColor: Color = .white, backgroundColor: Color = .blue, cornorRadius: Float? = nil, action: ((Button)->())? = nil) {
148149
super.init()
149150

151+
self.clipToBounds = true
152+
150153
if let size {
151154
self.widthAnchor.constrain(to: size.width)
152155
self.heightAnchor.constrain(to: size.height)
@@ -174,16 +177,23 @@ open class Button: Control {
174177
}
175178

176179
private var labelCreated: Bool = false
177-
public private(set) lazy var label: Label = {
180+
weak var _label: Label! = nil
181+
func createLabel() {
178182
let label = Label(text: "Button", font: .babel, fontSize: 14, style: .regular, textColor: self.textColors[.normal] ?? .white)
179183
label.centerXAnchor.constrain(to: self.centerXAnchor)
180184
label.centerYAnchor.constrain(to: self.centerYAnchor)
181185
label.widthAnchor.constrain(to: self.widthAnchor)
182186
label.heightAnchor.constrain(to: self.heightAnchor)
183187
self.addSubview(label)
184188
self.labelCreated = true
185-
return label
186-
}()
189+
_label = label
190+
}
191+
public var label: Label {
192+
if labelCreated == false {
193+
createLabel()
194+
}
195+
return _label
196+
}
187197
}
188198

189199

Sources/GateEngine/UI/Toggle.swift

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* Copyright © 2025 Dustin Collins (Strega's Gate)
3+
* All Rights Reserved.
4+
*
5+
* http://stregasgate.com
6+
*/
7+
8+
open class Toggle: Control {
9+
public var value: Bool = false {
10+
didSet {
11+
if self.state != .highlighted {
12+
if value {
13+
self.state = .selected
14+
}else{
15+
self.state = .normal
16+
}
17+
}
18+
}
19+
}
20+
21+
private var backgroundColors: [State: Color] = [
22+
.highlighted:.lightBlue,
23+
.normal:.blue,
24+
.selected:.darkBlue,
25+
]
26+
public func setBackgroundColor(_ color: Color, forState state: State) {
27+
backgroundColors[state] = color
28+
if self.state == state {
29+
self.backgroundColor = color
30+
}
31+
}
32+
public func setBackgroundColor(_ color: Color) {
33+
self.backgroundColor = color
34+
self.backgroundColors[.normal] = color
35+
self.backgroundColors[.highlighted] = color.interpolated(to: .lightGray, .linear(0.25))
36+
self.backgroundColors[.selected] = color.interpolated(to: .darkGray, .linear(0.25))
37+
}
38+
39+
private var textColors: [State: Color] = [
40+
.highlighted:.white,
41+
.normal:.white,
42+
.selected:.white,
43+
]
44+
public func setTextColor(_ color: Color, forState state: State) {
45+
textColors[state] = color
46+
if self.state == state {
47+
self.label.textColor = color
48+
}
49+
}
50+
51+
open override func canBeHit() -> Bool {
52+
return true
53+
}
54+
55+
public struct Event: OptionSet, Hashable, Sendable {
56+
public typealias RawValue = UInt
57+
public var rawValue: RawValue
58+
59+
public static let changed = Self(rawValue: 1 << 0)
60+
61+
public init(rawValue: RawValue) {
62+
self.rawValue = rawValue
63+
}
64+
}
65+
66+
private var eventActionStorage: [Event: [(Toggle)->()]] = [:]
67+
68+
public final func sendActions(forEvent event: Toggle.Event) {
69+
if let eventActionStorage = eventActionStorage[event] {
70+
unowned let unownedSelf = self
71+
for block in eventActionStorage {
72+
block(unownedSelf)
73+
}
74+
}
75+
}
76+
77+
public final func action(completion: @escaping (Toggle)->()) {
78+
var array = eventActionStorage[.changed] ?? []
79+
array.append(completion)
80+
eventActionStorage[.changed] = array
81+
}
82+
83+
public enum State {
84+
case normal
85+
case highlighted
86+
case selected
87+
}
88+
public var state: State = .normal {
89+
didSet {
90+
if state != oldValue {
91+
self.stateDidChange()
92+
if let color = backgroundColors[state] {
93+
self.backgroundColor = color
94+
}
95+
if labelCreated {
96+
if let color = textColors[state] {
97+
self.label.textColor = color
98+
}
99+
}
100+
}
101+
}
102+
}
103+
104+
open func stateDidChange() {
105+
106+
}
107+
108+
open override func cursorEntered(_ cursor: Mouse) {
109+
self.state = .highlighted
110+
}
111+
112+
open override func cursorExited(_ cursor: Mouse) {
113+
if value {
114+
self.state = .selected
115+
}else{
116+
self.state = .normal
117+
}
118+
}
119+
120+
open override func cursorButtonDown(button: MouseButton, mouse: Mouse) {
121+
if let mouseLocation = mouse.locationInView(self) {
122+
if self.bounds.contains(mouseLocation) {
123+
self.value.toggle()
124+
self.sendActions(forEvent: Self.Event.changed)
125+
}
126+
}
127+
self.state = .selected
128+
}
129+
130+
open override func cursorButtonUp(button: MouseButton, mouse: Mouse) {
131+
if mouse.isInsideView(self) {
132+
self.state = .highlighted
133+
}else if value {
134+
self.state = .selected
135+
}else{
136+
self.state = .normal
137+
}
138+
}
139+
140+
open override func touchesBegan(_ touches: Set<Touch>) {
141+
self.state = .selected
142+
}
143+
144+
open override func touchesMoved(_ touches: Set<Touch>) {
145+
if value {
146+
self.state = .selected
147+
}else{
148+
self.state = .normal
149+
}
150+
for touch in touches {
151+
if touch.isInsideView(self) {
152+
self.state = .selected
153+
}
154+
}
155+
}
156+
157+
open override func touchesEnded(_ touches: Set<Touch>) {
158+
if value {
159+
self.state = .selected
160+
}else{
161+
self.state = .normal
162+
}
163+
for touch in touches {
164+
if touch.isInsideView(self) {
165+
self.value.toggle()
166+
self.sendActions(forEvent: Self.Event.changed)
167+
break
168+
}
169+
}
170+
}
171+
172+
open override func touchesCanceled(_ touches: Set<Touch>) {
173+
if value {
174+
self.state = .selected
175+
}else{
176+
self.state = .normal
177+
}
178+
}
179+
180+
public init(size: Size2? = nil, label: String? = nil, textColor: Color = .white, backgroundColor: Color = .blue, cornorRadius: Float? = nil, action: ((Toggle)->())? = nil) {
181+
super.init()
182+
183+
self.clipToBounds = true
184+
185+
if let size {
186+
self.widthAnchor.constrain(to: size.width)
187+
self.heightAnchor.constrain(to: size.height)
188+
}
189+
190+
self.setBackgroundColor(backgroundColor)
191+
192+
self.textColors[.normal] = textColor
193+
self.textColors[.highlighted] = textColor
194+
self.textColors[.selected] = textColor
195+
196+
if let label {
197+
self.label.text = label
198+
self.label.textColor = textColor
199+
}
200+
if let action {
201+
self.eventActionStorage[.changed] = [action]
202+
}
203+
204+
if let cornorRadius {
205+
self.cornerRadius = cornorRadius
206+
}
207+
208+
self.stateDidChange()
209+
}
210+
211+
private var labelCreated: Bool = false
212+
public private(set) lazy var label: Label = {
213+
let label = Label(text: "Toggle", font: .babel, fontSize: 14, style: .regular, textColor: self.textColors[.normal] ?? .white)
214+
label.centerXAnchor.constrain(to: self.centerXAnchor)
215+
label.centerYAnchor.constrain(to: self.centerYAnchor)
216+
label.widthAnchor.constrain(to: self.widthAnchor)
217+
label.heightAnchor.constrain(to: self.heightAnchor)
218+
self.addSubview(label)
219+
self.labelCreated = true
220+
return label
221+
}()
222+
}
223+
224+

Sources/GateEngine/UI/View.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ open class View {
7373
}
7474
}
7575
internal var needsUpdateConstraints: Bool = true
76-
public private(set) var superView: View? = nil {
76+
public private(set) weak var superView: View? = nil {
7777
didSet {
7878
self._window = nil
7979
self.setNeedsLayout()
@@ -91,7 +91,7 @@ open class View {
9191
@usableFromInline
9292
internal final weak var _viewController: ViewController? = nil
9393

94-
public private(set) var _window: Window? = nil
94+
public private(set) weak var _window: Window? = nil
9595
public var window: Window? {
9696
if let _window {
9797
return _window
@@ -437,11 +437,11 @@ open class View {
437437
}
438438
internal final var offScreenRepresentationMaterialNeedsUpdate: Bool = true
439439
private func updateoffScreenRepresentationMaterial() {
440-
self._offScreenRepresentationMaterial.channel(0) { channel in
441-
channel.color = backgroundColor ?? .clear
440+
self._offScreenRepresentationMaterial.channel(0) { [weak self] channel in
441+
channel.color = self?.backgroundColor ?? .clear
442442
}
443443

444-
self._offScreenRepresentationMaterial.setCustomUniformValue(self.opacity, forUniform: "opacity")
444+
self._offScreenRepresentationMaterial.setCustomUniformValue(opacity, forUniform: "opacity")
445445
self._offScreenRepresentationMaterial.setCustomUniformValue(cornerRadius * interfaceScale, forUniform: "Radius")
446446

447447
self._offScreenRepresentationMaterial.setCustomUniformValue(cornerMask.contains(.topLeft), forUniform: "TopLeft")

0 commit comments

Comments
 (0)