Skip to content
Open
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
55 changes: 55 additions & 0 deletions BifcodePackage/Sources/BifcodeFeature/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
82 changes: 80 additions & 2 deletions BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down