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
56 changes: 56 additions & 0 deletions BifcodePackage/Sources/BifcodeFeature/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ 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("exportPreset") private var exportPresetRaw: String = ExportPreset.auto.rawValue

private var layout: WindowLayout {
WindowLayout(rawValue: windowLayoutRaw) ?? .horizontal
Expand All @@ -112,6 +113,10 @@ public struct ContentView: View {
ThemeMode(rawValue: themeModeRaw) ?? .dark
}

private var exportPreset: ExportPreset {
ExportPreset(rawValue: exportPresetRaw) ?? .auto
}

public init() {}

/// Check if both code panels are empty (export should be disabled)
Expand Down Expand Up @@ -445,8 +450,59 @@ public struct ContentView: View {
let finalImage = NSImage(size: size)
finalImage.addRepresentation(scaledBitmapRep)

// Apply export preset scaling if not auto
if let presetSize = exportPreset.size {
return scaleImageToPreset(finalImage, targetSize: presetSize)
}

return finalImage
}

/// Scales an image to fit within the target preset size while maintaining aspect ratio.
///
/// The image is scaled down to fit within the preset dimensions, centered on a transparent background.
/// If the content is smaller than the preset, it's centered without scaling up.
///
/// - Parameters:
/// - image: The source image to scale.
/// - targetSize: The target dimensions from the export preset.
/// - Returns: A new image at the exact preset dimensions.
@MainActor
private func scaleImageToPreset(_ 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 preset dimensions
let presetImage = NSImage(size: targetSize)
presetImage.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
)

presetImage.unlockFocus()

return presetImage
}
}

// MARK: - Preview
Expand Down
80 changes: 80 additions & 0 deletions BifcodePackage/Sources/BifcodeFeature/Models/SettingsTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,86 @@ public enum ThemeMode: String, CaseIterable, Sendable {
}
}

// MARK: - Export Preset

/// Preset export dimensions for different platforms.
///
/// Provides optimized dimensions for sharing code comparisons on
/// various social media platforms and messaging apps.
///
/// ## Usage
///
/// ```swift
/// @AppStorage("exportPreset") private var exportPreset = ExportPreset.auto.rawValue
/// ```
///
/// ## Topics
///
/// ### Presets
///
/// - ``auto``
/// - ``twitter``
/// - ``linkedin``
/// - ``slack``
public enum ExportPreset: String, CaseIterable, Sendable {
/// Automatic sizing based on content.
///
/// The default behavior that calculates dimensions from the
/// actual code content and settings.
case auto

/// Twitter/X optimized dimensions (1200×675).
///
/// Optimal for Twitter image cards and timeline display.
case twitter

/// LinkedIn optimized dimensions (1200×627).
///
/// Matches LinkedIn's recommended image dimensions for posts.
case linkedin

/// Slack optimized dimensions (800×600).
///
/// Works well in Slack messages and previews.
case slack

/// A human-readable label for display in pickers.
public var label: String {
switch self {
case .auto: "Auto"
case .twitter: "Twitter"
case .linkedin: "LinkedIn"
case .slack: "Slack"
}
}

/// The target width for this preset, or nil for auto.
public var width: CGFloat? {
switch self {
case .auto: nil
case .twitter: 1200
case .linkedin: 1200
case .slack: 800
}
}

/// The target height for this preset, or nil for auto.
public var height: CGFloat? {
switch self {
case .auto: nil
case .twitter: 675
case .linkedin: 627
case .slack: 600
}
}

/// The target size for this preset, or nil for auto.
public var size: CGSize? {
guard let width, let height else { return nil }
return CGSize(width: width, height: height)
}
}

// MARK: - Editor Themes

/// Available syntax highlighting themes for the code editor.
Expand Down
17 changes: 16 additions & 1 deletion BifcodePackage/Sources/BifcodeFeature/Views/ToolBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ 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("exportPreset") private var exportPresetRaw: String = ExportPreset.auto.rawValue

@AppStorage("selectedLanguage") private var selectedLanguageRaw: String = "swift"

Expand Down Expand Up @@ -546,6 +547,8 @@ public struct ToolBarView: View {

// Secondary Actions
VStack(spacing: 8) {
exportPresetPicker

Button {
chooseSaveLocation()
} label: {
Expand All @@ -561,7 +564,19 @@ public struct ToolBarView: View {
.padding(.top, 32)
}
}
.frame(minWidth: 80)
.frame(minWidth: 100)
}

private var exportPresetPicker: some View {
Picker("", selection: $exportPresetRaw) {
ForEach(ExportPreset.allCases, id: \.self) { preset in
Text(preset.label).tag(preset.rawValue)
}
}
.pickerStyle(.menu)
.fixedSize()
.accessibilityLabel("Export Size")
.accessibilityHint("Choose preset dimensions for social media")
}

private var storeButton: some View {
Expand Down