Skip to content

Commit

Permalink
Improve settings
Browse files Browse the repository at this point in the history
  • Loading branch information
BarredEwe committed Dec 9, 2024
1 parent 73fb23b commit cb0ef42
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 85 deletions.
107 changes: 58 additions & 49 deletions SnowfallApp/Common/MenuBarSettings.swift
Original file line number Diff line number Diff line change
@@ -1,70 +1,79 @@
import SwiftUI

struct MenuBarSettings: View {
@State var speed: Float = Settings.snowflakeSpeedRange.upperBound
@State var size: Float = Settings.snowflakeSizeRange.upperBound
@State var speed: ClosedRange<Float> = Settings.snowflakeSpeedRange
@State var size: ClosedRange<Float> = Settings.snowflakeSizeRange
@State var maxSnowflakes: Float = Float(Settings.maxSnowflakes)
@State var windowInteraction: Bool = Settings.windowInteraction
@State var windStrength: Float = Settings.windStrength * 1000

var body: some View {
VStack(spacing: 2) {
VStack(alignment: .leading, spacing: 2) {
Group {
Row(title: "Snowflake speed: \(speed)")
.foregroundColor(.secondary)
Slider(value: $speed, in: 1...20.0)
.padding(.horizontal)
.padding(.vertical, 4)
}
VStack(alignment: .leading, spacing: 12) {
VStack(alignment: .leading) {
Text("Snowflake speed")
RangedSliderView(value: $speed, in: 1...20)
Divider()
}
VStack(alignment: .leading, spacing: 2) {
Group {
Row(title: "Snowflake size: \(size)")
.foregroundColor(.secondary)
Slider(value: $size, in: 4...40.0)
.padding(.horizontal)
.padding(.vertical, 4)
}

VStack(alignment: .leading) {
Text("Snowflake size")
RangedSliderView(value: $size, in: 3...40)
Divider()
}

VStack(alignment: .leading) {
Text("Snowflake count \(Int(maxSnowflakes))")
Slider(value: $maxSnowflakes, in: 1...4000)
Divider()
}

VStack(alignment: .leading, spacing: 2) {
Group {
Row(title: "Snowflake count: \(maxSnowflakes)")
.foregroundColor(.secondary)
Slider(value: $maxSnowflakes, in: 1...4000.0)
.padding(.horizontal)
.padding(.vertical, 4)
VStack(alignment: .leading) {
Text("Wind strength \(Int(windStrength / 100))")
Slider(value: $windStrength, in: 1...4000)
Divider()
}

Toggle("Snow interact with window", isOn: $windowInteraction)

HStack {
Button("Close app") {
NSApplication.shared.terminate(nil)
}
.buttonStyle(.borderless)

Spacer()

Button("Reset", role: .destructive) {
Settings.reset()
speed = Settings.snowflakeSpeedRange
size = Settings.snowflakeSizeRange
maxSnowflakes = Float(Settings.maxSnowflakes)
windowInteraction = Settings.windowInteraction
windStrength = Settings.windStrength * 1000
}
}
}
.frame(maxWidth: 250)
.padding(.vertical, 4)
.onChange(of: speed) { oldValue, newValue in
Settings.snowflakeSpeedRange = Settings.snowflakeSpeedRange.lowerBound...newValue
.padding()
.onChange(of: speed) { _, newValue in
Settings.snowflakeSpeedRange = newValue
}
.onChange(of: size) { oldValue, newValue in
Settings.snowflakeSizeRange = Settings.snowflakeSizeRange.lowerBound...newValue
.onChange(of: size) { _, newValue in
Settings.snowflakeSizeRange = newValue
}
.onChange(of: maxSnowflakes) { oldValue, newValue in
.onChange(of: maxSnowflakes) { _, newValue in
Settings.maxSnowflakes = Int(newValue)
}
.onChange(of: windowInteraction) { _, newValue in
Settings.windowInteraction = newValue
}
.onChange(of: windStrength) { _, newValue in
Settings.windStrength = newValue / 1000
}
}
}

struct Row: View {
let title : String
var hasCheckmark = false

var body: some View {
HStack(spacing: 4) {
Group {
if hasCheckmark {
Image(systemName: "checkmark")
} else {
Text("")
}
}
.frame(width: 12)
Text(title)
}
}
#Preview {
MenuBarSettings()
.scaledToFit()
}
2 changes: 1 addition & 1 deletion SnowfallApp/Common/MouseTrackingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class MouseTrackingNSView: NSView {

let trackingArea = NSTrackingArea(
rect: bounds,
options: [.activeAlways, .mouseMoved, .inVisibleRect],
options: [.activeAlways, .mouseMoved, .mouseEnteredAndExited, .inVisibleRect, .activeWhenFirstResponder],
owner: self,
userInfo: nil
)
Expand Down
98 changes: 98 additions & 0 deletions SnowfallApp/Common/RangedSliderView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import SwiftUI

struct RangedSliderView: View {
let currentValue: Binding<ClosedRange<Float>>
let sliderBounds: ClosedRange<Int>

public init(value: Binding<ClosedRange<Float>>, in bounds: ClosedRange<Int>) {
self.currentValue = value
self.sliderBounds = bounds
}

var body: some View {
GeometryReader { geomentry in
sliderView(sliderSize: geomentry.size)

}
.padding(.init(top: 16, leading: 10, bottom: 6, trailing: 10))
}


@ViewBuilder private func sliderView(sliderSize: CGSize) -> some View {
let sliderViewYCenter = sliderSize.height / 2
ZStack {
RoundedRectangle(cornerRadius: 2)
.fill(Color.secondary.opacity(0.2))
.frame(height: 4)
.padding(.horizontal, -10)

ZStack {
let sliderBoundDifference = sliderBounds.count
let stepWidthInPixel = CGFloat(sliderSize.width) / CGFloat(sliderBoundDifference)

// Calculate Left Thumb initial position
let leftThumbLocation: CGFloat = currentValue.wrappedValue.lowerBound == Float(sliderBounds.lowerBound)
? 0
: CGFloat(currentValue.wrappedValue.lowerBound - Float(sliderBounds.lowerBound)) * stepWidthInPixel

// Calculate right thumb initial position
let rightThumbLocation = CGFloat(currentValue.wrappedValue.upperBound) * stepWidthInPixel

// Path between both handles
lineBetweenThumbs(from: .init(x: leftThumbLocation, y: sliderViewYCenter), to: .init(x: rightThumbLocation, y: sliderViewYCenter))

// Left Thumb Handle
let leftThumbPoint = CGPoint(x: leftThumbLocation, y: sliderViewYCenter)
thumbView(position: leftThumbPoint, value: Float(currentValue.wrappedValue.lowerBound))
.highPriorityGesture(DragGesture().onChanged { dragValue in

let dragLocation = dragValue.location
let xThumbOffset = min(max(0, dragLocation.x), sliderSize.width)

let newValue = Float(sliderBounds.lowerBound) + Float(xThumbOffset / stepWidthInPixel)

// Stop the range thumbs from colliding each other
if newValue < currentValue.wrappedValue.upperBound {
currentValue.wrappedValue = newValue...currentValue.wrappedValue.upperBound
}
})

// Right Thumb Handle
thumbView(position: CGPoint(x: rightThumbLocation, y: sliderViewYCenter), value: currentValue.wrappedValue.upperBound)
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(max(CGFloat(leftThumbLocation), dragLocation.x), sliderSize.width)

var newValue = Float(xThumbOffset / stepWidthInPixel) // convert back the value bound
newValue = min(newValue, Float(sliderBounds.upperBound))

// Stop the range thumbs from colliding each other
if newValue > currentValue.wrappedValue.lowerBound {
currentValue.wrappedValue = currentValue.wrappedValue.lowerBound...newValue
}
})
}
}
}

@ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
Path { path in
path.move(to: from)
path.addLine(to: to)
}.stroke(Color.accentColor, lineWidth: 4)
}

@ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
ZStack {
Text(String(Int(value)))
.font(.system(size: 10, weight: .semibold))
.offset(y: -20)
Circle()
.frame(width: 20, height: 20)
.foregroundColor(Color.gray)
.shadow(color: Color.black.opacity(0.16), radius: 8, x: 0, y: 2)
.contentShape(Rectangle())
}
.position(x: position.x, y: position.y)
}
}
43 changes: 41 additions & 2 deletions SnowfallApp/Common/Settings.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,47 @@
import Cocoa
import SwiftUI

struct Settings {
static var snowflakeSizeRange: ClosedRange<Float> = 4...10
@AppStorage("snowflakeSizeRange")
static var snowflakeSizeRange: ClosedRange<Float> = 3...13
@AppStorage("maxSnowflakes")
static var maxSnowflakes = 1000
@AppStorage("snowflakeSpeedRange")
static var snowflakeSpeedRange: ClosedRange<Float> = 1...3
static var windStrength: Float = 0.5
@AppStorage("windStrength")
static var windStrength: Float = 2.5
@AppStorage("semltingSpeed")
static var semltingSpeed: Float = 0.05
@AppStorage("windowInteraction")
static var windowInteraction: Bool = true

static func reset() {
UserDefaults.standard.dictionaryRepresentation().keys.forEach({ UserDefaults.standard.removeObject(forKey: $0) })
}
}

// MARK: - Extensions

extension Float: @retroactive RawRepresentable {
public var rawValue: String {
String(self)
}

public typealias RawValue = String

public init?(rawValue: String) {
self = Float(rawValue)!
}
}

extension ClosedRange: @retroactive RawRepresentable where Bound == Float {
public init?(rawValue: String) {
let split = rawValue.split(separator: "...")

self.init(uncheckedBounds: (lower: Float(split[0])!, upper: Float(split[1])!))
}

public var rawValue: String {
return String(lowerBound) + "..." + String(upperBound)
}
}
74 changes: 52 additions & 22 deletions SnowfallApp/Common/SnowRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ class SnowRenderer: NSObject, MTKViewDelegate {
private var commandQueue: MTLCommandQueue!
private var particleBuffer: MTLBuffer!
private var pipelineState: MTLRenderPipelineState!

private var snowflakes: [Snowflake] = []

var mousePosition: simd_float2 = .zero
var mousePosition: simd_float2 = simd_float2(.infinity, .infinity)
var screenSize: simd_float2 = .zero

private let influenceRadius: Float = 50.0
Expand Down Expand Up @@ -83,8 +84,14 @@ class SnowRenderer: NSObject, MTKViewDelegate {

let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)!
renderEncoder.setRenderPipelineState(pipelineState)

// Snowflakes
renderEncoder.setVertexBuffer(particleBuffer, offset: 0, index: 0)

// ScreenSize
renderEncoder.setVertexBytes(&screenSize, length: MemoryLayout<SIMD2<Float>>.stride, index: 1)
renderEncoder.setFragmentBytes(&screenSize, length: MemoryLayout<SIMD2<Float>>.stride, index: 1)

renderEncoder.drawPrimitives(type: .point, vertexStart: 0, vertexCount: Settings.maxSnowflakes)
renderEncoder.endEncoding()

Expand All @@ -96,6 +103,12 @@ class SnowRenderer: NSObject, MTKViewDelegate {

func updateSnowflakes() {
let radius = influenceRadius
var activeWindowRect: CGRect?

if Settings.windowInteraction {
activeWindowRect = WindowInfo().getActiveWindowRect()
}

for i in 0..<Settings.maxSnowflakes {
var snowflake = snowflakes[i]
let dx = mousePosition.x - snowflake.position.x
Expand All @@ -112,9 +125,16 @@ class SnowRenderer: NSObject, MTKViewDelegate {
}

snowflake.position.y += snowflake.velocity.y
snowflake.position.x += Float.random(in: 0.1...1) * Settings.windStrength
snowflake.position.x += Float.random(in: 0.1...0.5) * Settings.windStrength

if Settings.windowInteraction,
let activeWindowRect,
activeWindowRect.contains(CGPoint(x: CGFloat(snowflake.position.x), y: CGFloat(snowflake.position.y))) {
snowflake.position.y = Float(activeWindowRect.origin.y)
snowflake.size -= Settings.semltingSpeed
}

if snowflake.position.y - snowflake.size > screenSize.y {
if (snowflake.position.y - snowflake.size > screenSize.y) || !(snowflake.size > Settings.snowflakeSizeRange.lowerBound - 1) {
snowflake.clear(for: screenSize)
}

Expand All @@ -128,24 +148,34 @@ class SnowRenderer: NSObject, MTKViewDelegate {
}
}

// MARK: Extensions

extension simd_float2 {
static var velocity: simd_float2 {
simd_float2(Float.random(in: -1...1), Float.random(in: Settings.snowflakeSpeedRange))
}
}

extension Float {
static var size: Float {
Float.random(in: Settings.snowflakeSizeRange)
}
}

extension simd_float4 {
static func color(for size: Float) -> simd_float4 {
let normalizedSize = (size - Settings.snowflakeSizeRange.lowerBound) / (Settings.snowflakeSizeRange.upperBound - Settings.snowflakeSizeRange.lowerBound)
let opacity = Swift.max(0.1, 1.0 - normalizedSize * 0.8)
return simd_float4(1.0, 1.0, 1.0, opacity)
import Cocoa
import CoreGraphics

class WindowInfo {
let statusBarSize = 38.0

func getActiveWindowRect() -> CGRect? {
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
let windowListInfo = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] ?? []

for windowInfo in windowListInfo {
guard let layer = windowInfo[kCGWindowLayer as String] as? Int,
layer == 0,
// let ownerName = windowInfo[kCGWindowOwnerName as String] as? String,
let windowBounds = windowInfo[kCGWindowBounds as String] as? [String: Any],
let x = windowBounds["X"] as? CGFloat,
let y = windowBounds["Y"] as? CGFloat,
let width = windowBounds["Width"] as? CGFloat,
// let height = windowBounds["Height"] as? CGFloat,
!(x == 0 && (y == statusBarSize || y == 0)), width >= 50 else { continue }

// print("App: \(ownerName)")
// print("Position: (\(x), \(y))")
// print("Size: \(width)x\(height)")
// print("----------------------")

return CGRect(x: x, y: y, width: width, height: 50.0)
}
return nil
}
}
Loading

0 comments on commit cb0ef42

Please sign in to comment.