Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions Sources/FreeDock/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
.appendingPathComponent(".config/freedock.json")
)
private var _lockPositions = false

private var dockStates: [UUID: DockState] = [:]
private struct IconSizeSelection {
let dockID: UUID
let size: Double
Expand Down Expand Up @@ -97,8 +97,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {

let iconItem = NSMenuItem(title: "Icon Size", action: nil, keyEquivalent: "")
iconItem.submenu = iconMenu
dockMenu.addItem(iconItem)

let root = NSMenuItem(title: dock.name, action: nil, keyEquivalent: "")
root.submenu = dockMenu

Expand Down Expand Up @@ -251,19 +249,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc private func changeIconSize(_ sender: NSMenuItem) {
guard let selection = sender.representedObject as? IconSizeSelection,
let index = configManager.config.docks.firstIndex(where: { $0.id == selection.dockID })
else {
return
}
else { return }

configManager.config.docks[index].iconSize = selection.size
configManager.save()

if let panel = dockPanels[selection.dockID] {
panel.close()
dockPanels.removeValue(forKey: selection.dockID)
}

showDock(configManager.config.docks[index])
dockStates[selection.dockID]?.iconSize = selection.size
dockPanels[selection.dockID]?.resizeToFitContent()
rebuildMenu()
}

Expand All @@ -274,6 +266,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
panel.clampToVisibleFrame()

let dockID = config.id
let state = DockState(iconSize: config.iconSize)
dockStates[config.id] = state // ← store it

let content = DockContentView(
panel: panel,
items: Binding(
Expand All @@ -284,7 +279,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
}
),
orientation: config.orientation,
iconSize: config.iconSize,
state: state,
onItemsChanged: { _ in
self.configManager.save()
DispatchQueue.main.async { panel.resizeToFitContent() }
Expand Down Expand Up @@ -332,4 +327,27 @@ extension AppDelegate: DockPanelDelegate {
configManager.config.docks[idx].position = snapped.origin
configManager.save()
}

func currentIconSize(for panel: DockPanel) -> Double {
dockStates[panel.dockID]?.iconSize ?? 48
}

func dockPanelDidResize(_ panel: DockPanel, proposedIconSize: Double) {
guard let idx = configManager.config.docks.firstIndex(where: { $0.id == panel.dockID }) else { return }

// Update reactive state → SwiftUI reflows content
dockStates[panel.dockID]?.iconSize = proposedIconSize
configManager.config.docks[idx].iconSize = proposedIconSize

// Fit panel to the new content size
DispatchQueue.main.async {
panel.resizeToFitContent()
}
}

func dockPanelDidFinishResize(_ panel: DockPanel) {
guard let idx = configManager.config.docks.firstIndex(where: { $0.id == panel.dockID }) else { return }
configManager.config.docks[idx].position = panel.frame.origin
configManager.save()
}
}
15 changes: 0 additions & 15 deletions Sources/FreeDock/Managers/TooltipManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,3 @@ final class TooltipManager {
panel = p
}
}

/// Helper to read a SwiftUI view's screen rect via AppKit
struct ScreenRectReader: NSViewRepresentable {
let onRect: (NSRect) -> Void
func makeNSView(context _: Context) -> NSView {
NSView()
}

func updateNSView(_ nsView: NSView, context _: Context) {
DispatchQueue.main.async {
guard let win = nsView.window else { return }
onRect(win.convertToScreen(nsView.convert(nsView.bounds, to: nil)))
}
}
}
10 changes: 10 additions & 0 deletions Sources/FreeDock/Models/DockState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SwiftUI

@MainActor
class DockState: ObservableObject {
@Published var iconSize: Double

init(iconSize: Double) {
self.iconSize = iconSize
}
}
5 changes: 5 additions & 0 deletions Sources/FreeDock/Utilities/Extension.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
extension Comparable {
func clamped(to range: ClosedRange<Self>) -> Self {
min(max(self, range.lowerBound), range.upperBound)
}
}
20 changes: 17 additions & 3 deletions Sources/FreeDock/Views/DockContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ struct DockContentView: View {
let panel: DockPanel
@Binding var items: [DockItem]
let orientation: Orientation
let iconSize: Double
@ObservedObject var state: DockState
let onItemsChanged: @MainActor ([DockItem]) -> Void
let onAppLaunch: @MainActor (DockItem) -> Void

private var iconSize: Double {
state.iconSize
}

@State private var isTargeted = false
@State private var dropPulse = false
@State private var draggedItem: DockItem?
Expand All @@ -31,6 +35,11 @@ struct DockContentView: View {
.frame(width: 24)
HStack(spacing: 0) { content }
.padding(4)
DockResizeHandleRepresentable(
panel: panel,
orientation: orientation
)
.frame(width: 12)
}
.contentShape(Rectangle())
.onHover { hovering in
Expand Down Expand Up @@ -66,6 +75,11 @@ struct DockContentView: View {
.padding(.top, 12)
VStack(spacing: 0) { content }
.padding(4)
DockResizeHandleRepresentable(
panel: panel,
orientation: orientation
)
.frame(height: 12)
}
.contentShape(Rectangle())
.onHover { hovering in
Expand Down Expand Up @@ -214,8 +228,8 @@ struct DockContentView: View {
}

switch abs(index - hoveredIndex) {
case 0: return 1.30
case 1: return 1.15
case 0: return 1.20
case 1: return 1.10
case 2: return 1.05
default: return 1.0
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/FreeDock/Views/DockItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ struct DockItemView: View {
.animation(.easeOut(duration: 0.15), value: isHovering)
.padding(4)
.scaleEffect(scale)
.offset(y: bouncing ? -8 : 0)
.animation(.interpolatingSpring(stiffness: 300, damping: 8), value: bouncing)
.offset(y: bouncing ? -4 : 0)
.animation(.interpolatingSpring(stiffness: 300, damping: 4), value: bouncing)
.animation(.spring(response: 0.25, dampingFraction: 0.7), value: scale)
.offset(y: isHovering ? -2 : 0)
.shadow(radius: isHovering ? 5 : 2)
Expand Down
20 changes: 20 additions & 0 deletions Sources/FreeDock/Views/DockResizeHandleRepresentable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import AppKit
import SwiftUI

@MainActor
struct DockResizeHandleRepresentable: NSViewRepresentable {
let panel: DockPanel
let orientation: Orientation

func makeNSView(context _: Context) -> DockResizeHandleView {
let view = DockResizeHandleView(frame: .zero)
view.dockPanel = panel
view.orientation = orientation
return view
}

func updateNSView(_ view: DockResizeHandleView, context _: Context) {
view.dockPanel = panel
view.orientation = orientation
}
}
17 changes: 17 additions & 0 deletions Sources/FreeDock/Views/ScreenRectReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import AppKit
import SwiftUI

/// Helper to read a SwiftUI view's screen rect via AppKit
struct ScreenRectReader: NSViewRepresentable {
let onRect: (NSRect) -> Void
func makeNSView(context _: Context) -> NSView {
NSView()
}

func updateNSView(_ nsView: NSView, context _: Context) {
DispatchQueue.main.async {
guard let win = nsView.window else { return }
onRect(win.convertToScreen(nsView.convert(nsView.bounds, to: nil)))
}
}
}
5 changes: 3 additions & 2 deletions Sources/FreeDock/Windows/DockDragHandleView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ class DockDragHandleView: NSView {

override func layout() {
super.layout()
let pillW: CGFloat = 3
let pillH: CGFloat = 22
let pillW: CGFloat = 5
let pillH = CGFloat(22)

if orientation == .horizontal {
highlightLayer.frame = NSRect(
x: (stripRect.width - pillW) / 2 + stripRect.minX,
Expand Down
18 changes: 0 additions & 18 deletions Sources/FreeDock/Windows/DockPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ class DockPanel: NSPanel {
var dockOrientation: Orientation = .horizontal
weak var dockDelegate: DockPanelDelegate?
private weak var hostingView: NSView?
private var containerView: NSView?
private var hideWorkItem: DispatchWorkItem?

private func enforcedSize(for intrinsicSize: NSSize) -> NSSize {
Expand Down Expand Up @@ -49,15 +48,13 @@ class DockPanel: NSPanel {
func setContentView<V: View>(_ view: V) {
let container = DockContainerView(frame: NSRect(origin: .zero, size: NSSize(width: 400, height: 70)))
container.dockPanel = self
containerView = container

let hosting = NSHostingView(rootView: view)
hosting.frame = container.bounds
hosting.autoresizingMask = [.width, .height]
hosting.wantsLayer = true
hosting.layer?.cornerRadius = 14
container.addSubview(hosting)
container.addSubview(hosting)
hostingView = hosting
contentView = container
let intrinsicSize = hosting.intrinsicContentSize
Expand All @@ -80,21 +77,6 @@ class DockPanel: NSPanel {
setContentSize(enforcedSize)
}

func addDragHandle(orientation: Orientation) {
guard let container = containerView else {
os_log(.error, "containerView is nil")
return
}
let handle = DockDragHandleView(frame: container.bounds)

handle.autoresizingMask = [.width, .height]
handle.orientation = orientation
handle.wantsLayer = true
handle.layer?.backgroundColor = NSColor.clear.cgColor
handle.dockPanel = self
container.addSubview(handle, positioned: .above, relativeTo: nil)
}

/// Prevent docks from landing off-screen (e.g., after monitor disconnect)
func clampToVisibleFrame() {
guard let screen = NSScreen.main else { return }
Expand Down
3 changes: 3 additions & 0 deletions Sources/FreeDock/Windows/DockPanelDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ import Foundation
protocol DockPanelDelegate: AnyObject {
var lockPositions: Bool { get }
func dockPanelDidMove(_ panel: DockPanel)
func dockPanelDidResize(_ panel: DockPanel, proposedIconSize: Double)
func dockPanelDidFinishResize(_ panel: DockPanel)
func currentIconSize(for panel: DockPanel) -> Double
}
51 changes: 51 additions & 0 deletions Sources/FreeDock/Windows/DockResizeHandleView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Cocoa

class DockResizeHandleView: NSView {
weak var dockPanel: DockPanel?
var orientation: Orientation = .horizontal

private var dragStartLocation: NSPoint = .zero
private var dragStartIconSize: Double = 48

override init(frame: NSRect) {
super.init(frame: frame)
wantsLayer = true
layer?.backgroundColor = NSColor.clear.cgColor
}

required init?(coder _: NSCoder) {
nil
}

override func resetCursorRects() {
discardCursorRects()
addCursorRect(bounds, cursor: orientation == .horizontal ? .resizeLeftRight : .resizeUpDown)
}

override func acceptsFirstMouse(for _: NSEvent?) -> Bool {
true
}

override func mouseDown(with _: NSEvent) {
guard let panel = dockPanel else { return }
dragStartLocation = NSEvent.mouseLocation
// Snapshot the current icon size at drag start
dragStartIconSize = panel.dockDelegate?.currentIconSize(for: panel) ?? 48
}

override func mouseDragged(with _: NSEvent) {
guard let panel = dockPanel else { return }
let current = NSEvent.mouseLocation
let delta = orientation == .horizontal
? current.x - dragStartLocation.x
: -(current.y - dragStartLocation.y) // drag down = smaller for vertical

let newSize = (dragStartIconSize + delta * 0.4).clamped(to: 16 ... 128)
panel.dockDelegate?.dockPanelDidResize(panel, proposedIconSize: newSize)
}

override func mouseUp(with _: NSEvent) {
guard let panel = dockPanel else { return }
panel.dockDelegate?.dockPanelDidFinishResize(panel)
}
}
Loading