diff --git a/BifcodePackage/Sources/BifcodeFeature/ContentView.swift b/BifcodePackage/Sources/BifcodeFeature/ContentView.swift index 24e6a78..8e1ad65 100644 --- a/BifcodePackage/Sources/BifcodeFeature/ContentView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/ContentView.swift @@ -91,6 +91,9 @@ public struct ContentView: View { @AppStorage("fontSize") private var fontSize: Double = 14 @AppStorage("selectedTheme") private var selectedThemeRaw: String = EditorThemeOption.atomOneDark.rawValue @AppStorage("themeMode") private var themeModeRaw: String = ThemeMode.dark.rawValue + @AppStorage("useCustomDimensions") private var useCustomDimensions: Bool = false + @AppStorage("customExportWidth") private var customExportWidth: Double = 1200 + @AppStorage("customExportHeight") private var customExportHeight: Double = 675 private var layout: WindowLayout { WindowLayout(rawValue: windowLayoutRaw) ?? .horizontal @@ -445,8 +448,60 @@ public struct ContentView: View { let finalImage = NSImage(size: size) finalImage.addRepresentation(scaledBitmapRep) + // Apply custom dimensions scaling if enabled + if useCustomDimensions { + let targetSize = CGSize(width: customExportWidth, height: customExportHeight) + return scaleImageToCustomSize(finalImage, targetSize: targetSize) + } + return finalImage } + + /// Scales an image to fit within the target custom size while maintaining aspect ratio. + /// + /// The image is scaled to fit within the custom dimensions, centered on a transparent background. + /// Content is scaled down if larger than target, or centered without scaling if smaller. + /// + /// - Parameters: + /// - image: The source image to scale. + /// - targetSize: The target dimensions from custom export settings. + /// - Returns: A new image at the exact custom dimensions. + @MainActor + private func scaleImageToCustomSize(_ image: NSImage, targetSize: CGSize) -> NSImage { + let sourceSize = image.size + + // Calculate scale to fit within target while maintaining aspect ratio + let widthRatio = targetSize.width / sourceSize.width + let heightRatio = targetSize.height / sourceSize.height + let scale = min(widthRatio, heightRatio, 1.0) // Don't scale up, only down + + let scaledWidth = sourceSize.width * scale + let scaledHeight = sourceSize.height * scale + + // Center the scaled content within the target size + let xOffset = (targetSize.width - scaledWidth) / 2 + let yOffset = (targetSize.height - scaledHeight) / 2 + + // Create the final image at custom dimensions + let customImage = NSImage(size: targetSize) + customImage.lockFocus() + + // Clear background (transparent) + NSColor.clear.setFill() + NSRect(origin: .zero, size: targetSize).fill() + + // Draw the scaled image centered + image.draw( + in: NSRect(x: xOffset, y: yOffset, width: scaledWidth, height: scaledHeight), + from: .zero, + operation: .copy, + fraction: 1.0 + ) + + customImage.unlockFocus() + + return customImage + } } // MARK: - Preview diff --git a/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift b/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift index 1232ac2..976e090 100644 --- a/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift +++ b/BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift @@ -60,6 +60,10 @@ public struct ToolBarView: View { @AppStorage("fontSize") private var fontSize: Double = 14 @AppStorage("selectedTheme") private var selectedThemeRaw: String = EditorThemeOption.atomOneDark.rawValue @AppStorage("themeMode") private var themeModeRaw: String = ThemeMode.dark.rawValue + @AppStorage("useCustomDimensions") private var useCustomDimensions: Bool = false + @AppStorage("customExportWidth") private var customExportWidth: Double = 1200 + @AppStorage("customExportHeight") private var customExportHeight: Double = 675 + @AppStorage("lockAspectRatio") private var lockAspectRatio: Bool = true @AppStorage("selectedLanguage") private var selectedLanguageRaw: String = "swift" @@ -544,6 +548,19 @@ public struct ToolBarView: View { ? "Disabled: Add code to enable export" : "Save comparison image to selected folder") + // Custom Dimensions Toggle + Toggle("Size", isOn: $useCustomDimensions) + .toggleStyle(.checkbox) + .font(.caption) + .help("Use custom export dimensions") + .accessibilityLabel("Custom Size") + .accessibilityHint(useCustomDimensions ? "Custom dimensions enabled" : "Auto-size based on content") + + // Custom Dimensions Fields + if useCustomDimensions { + customDimensionsFields + } + // Secondary Actions VStack(spacing: 8) { Button { @@ -558,10 +575,71 @@ public struct ToolBarView: View { .accessibilityHint("Select folder for exported images") storeButton - .padding(.top, 32) + .padding(.top, useCustomDimensions ? 8 : 32) + } + } + .frame(minWidth: 120) + } + + /// Stores the aspect ratio when locking is enabled + @State private var lockedAspectRatio: Double = 1200.0 / 675.0 + + private var customDimensionsFields: some View { + VStack(spacing: 6) { + // Width field + HStack(spacing: 4) { + Text("W") + .font(.caption2) + .foregroundStyle(Color.tertiaryText) + .frame(width: 12) + TextField("Width", value: $customExportWidth, format: .number) + .textFieldStyle(.roundedBorder) + .frame(width: 60) + .onChange(of: customExportWidth) { oldValue, newValue in + if lockAspectRatio, oldValue != newValue { + let clampedWidth = min(max(newValue, 100), 4096) + customExportHeight = clampedWidth / lockedAspectRatio + } + } + } + .accessibilityElement(children: .combine) + .accessibilityLabel("Width") + .accessibilityValue("\(Int(customExportWidth)) pixels") + + // Height field + HStack(spacing: 4) { + Text("H") + .font(.caption2) + .foregroundStyle(Color.tertiaryText) + .frame(width: 12) + TextField("Height", value: $customExportHeight, format: .number) + .textFieldStyle(.roundedBorder) + .frame(width: 60) + .onChange(of: customExportHeight) { oldValue, newValue in + if lockAspectRatio, oldValue != newValue { + let clampedHeight = min(max(newValue, 100), 4096) + customExportWidth = clampedHeight * lockedAspectRatio + } + } + } + .accessibilityElement(children: .combine) + .accessibilityLabel("Height") + .accessibilityValue("\(Int(customExportHeight)) pixels") + + // Aspect ratio lock + Button { + lockAspectRatio.toggle() + if lockAspectRatio { + lockedAspectRatio = customExportWidth / max(customExportHeight, 1) + } + } label: { + Image(systemName: lockAspectRatio ? "lock.fill" : "lock.open") + .font(.caption) } + .buttonStyle(.borderless) + .help(lockAspectRatio ? "Unlock aspect ratio" : "Lock aspect ratio") + .accessibilityLabel(lockAspectRatio ? "Aspect ratio locked" : "Aspect ratio unlocked") } - .frame(minWidth: 80) } private var storeButton: some View {