From a34ebce6f4d0c65d1939210b57dd1d46b13b3ccd Mon Sep 17 00:00:00 2001 From: stackotter Date: Sun, 27 Jul 2025 23:53:05 +1000 Subject: [PATCH] Implement View.italic and Font.italic modifiers --- Sources/AppKitBackend/AppKitBackend.swift | 17 ++++++++++++--- Sources/Gtk/Utility/CSS/CSSProperty.swift | 4 ++++ Sources/Gtk3/Utility/CSS/CSSProperty.swift | 4 ++++ Sources/Gtk3Backend/Gtk3Backend.swift | 8 +++++++ Sources/GtkBackend/GtkBackend.swift | 8 +++++++ Sources/SwiftCrossUI/Values/Font.swift | 21 +++++++++++++++++-- .../Views/Modifiers/Style/FontModifier.swift | 10 +++++++++ Sources/UIKitBackend/Font+UIFont.swift | 15 +++++++++++-- Sources/WinUIBackend/WinUIBackend.swift | 6 ++++++ 9 files changed, 86 insertions(+), 7 deletions(-) diff --git a/Sources/AppKitBackend/AppKitBackend.swift b/Sources/AppKitBackend/AppKitBackend.swift index ad0af86945..469a2f3bbf 100644 --- a/Sources/AppKitBackend/AppKitBackend.swift +++ b/Sources/AppKitBackend/AppKitBackend.swift @@ -633,8 +633,11 @@ public final class AppKitBackend: AppBackend { onChange: @escaping (Bool) -> Void ) { let toggle = toggle as! NSButton + toggle.attributedTitle = Self.attributedString( + for: label, + in: environment.with(\.multilineTextAlignment, .center) + ) toggle.isEnabled = environment.isEnabled - toggle.title = label toggle.onAction = { toggle in let toggle = toggle as! NSButton onChange(toggle.state == .on) @@ -1131,15 +1134,23 @@ public final class AppKitBackend: AppBackend { private static func font(for font: Font.Resolved) -> NSFont { let size = CGFloat(font.pointSize) let weight = weight(for: font.weight) + + let nsFont: NSFont switch font.identifier.kind { case .system: switch font.design { case .default: - return NSFont.systemFont(ofSize: size, weight: weight) + nsFont = NSFont.systemFont(ofSize: size, weight: weight) case .monospaced: - return NSFont.monospacedSystemFont(ofSize: size, weight: weight) + nsFont = NSFont.monospacedSystemFont(ofSize: size, weight: weight) } } + + if font.isItalic { + return NSFontManager.shared.convert(nsFont, toHaveTrait: .italicFontMask) + } else { + return nsFont + } } private static func weight(for weight: Font.Weight) -> NSFont.Weight { diff --git a/Sources/Gtk/Utility/CSS/CSSProperty.swift b/Sources/Gtk/Utility/CSS/CSSProperty.swift index 5d6bee3cff..d0a3a2599e 100644 --- a/Sources/Gtk/Utility/CSS/CSSProperty.swift +++ b/Sources/Gtk/Utility/CSS/CSSProperty.swift @@ -70,6 +70,10 @@ public struct CSSProperty: Equatable { CSSProperty(key: "font-family", value: family) } + public static func fontStyle(_ style: String) -> CSSProperty { + CSSProperty(key: "font-style", value: style) + } + public static func rgba(_ color: Color) -> String { let red = color.red * 255 let green = color.green * 255 diff --git a/Sources/Gtk3/Utility/CSS/CSSProperty.swift b/Sources/Gtk3/Utility/CSS/CSSProperty.swift index 19c90f6367..8cfd5a868b 100644 --- a/Sources/Gtk3/Utility/CSS/CSSProperty.swift +++ b/Sources/Gtk3/Utility/CSS/CSSProperty.swift @@ -70,6 +70,10 @@ public struct CSSProperty: Equatable { CSSProperty(key: "font-family", value: family) } + public static func fontStyle(_ style: String) -> CSSProperty { + CSSProperty(key: "font-style", value: style) + } + public static func rgba(_ color: Color) -> String { let red = color.red * 255 let green = color.green * 255 diff --git a/Sources/Gtk3Backend/Gtk3Backend.swift b/Sources/Gtk3Backend/Gtk3Backend.swift index 248f412df8..ab504ef045 100644 --- a/Sources/Gtk3Backend/Gtk3Backend.swift +++ b/Sources/Gtk3Backend/Gtk3Backend.swift @@ -799,6 +799,10 @@ public final class Gtk3Backend: AppBackend { toggle.toggled = { widget in onChange(widget.active) } + toggle.css.clear() + // This is a control, but we set isControl to false anyway because isControl overrides + // the button background and makes the on and off states of the toggle look identical. + toggle.css.set(properties: Self.cssProperties(for: environment, isControl: false)) } public func setState(ofToggle toggle: Widget, to state: Bool) { @@ -1474,6 +1478,10 @@ public final class Gtk3Backend: AppBackend { } } + if font.isItalic { + properties.append(.fontStyle("italic")) + } + if isControl { switch environment.colorScheme { case .light: diff --git a/Sources/GtkBackend/GtkBackend.swift b/Sources/GtkBackend/GtkBackend.swift index 4329dd1600..f5ac86a878 100644 --- a/Sources/GtkBackend/GtkBackend.swift +++ b/Sources/GtkBackend/GtkBackend.swift @@ -775,6 +775,10 @@ public final class GtkBackend: AppBackend { toggle.toggled = { widget in onChange(widget.active) } + toggle.css.clear() + // This is a control, but we set isControl to false anyway because isControl overrides + // the button background and makes the on and off states of the toggle look identical. + toggle.css.set(properties: Self.cssProperties(for: environment, isControl: false)) } public func setState(ofToggle toggle: Widget, to state: Bool) { @@ -1525,6 +1529,10 @@ public final class GtkBackend: AppBackend { } } + if font.isItalic { + properties.append(.fontStyle("italic")) + } + if isControl { switch environment.colorScheme { case .light: diff --git a/Sources/SwiftCrossUI/Values/Font.swift b/Sources/SwiftCrossUI/Values/Font.swift index 5a406e18e4..b9187f3d98 100644 --- a/Sources/SwiftCrossUI/Values/Font.swift +++ b/Sources/SwiftCrossUI/Values/Font.swift @@ -56,6 +56,13 @@ public struct Font: Hashable, Sendable { return font } + /// Selects whether or not to italicize the font. + public func italic(_ italic: Bool = true) -> Font { + var font = self + font.overlay.italicize = italic + return font + } + /// Overrides the font's weight. Takes an optional for convenience. Does /// nothing if given `nil`. public func weight(_ weight: Weight?) -> Font { @@ -167,6 +174,10 @@ public struct Font: Hashable, Sendable { /// overlay has been applied if one is present. var emphasize: Bool = false + /// If `true`, overrides the font to be italicized. If `false`, does + /// nothing. + var italicize: Bool = false + /// Overrides the font's design. var design: Design? @@ -185,6 +196,9 @@ public struct Font: Hashable, Sendable { if emphasize { resolvedFont.weight = emphasizedWeight } + if italicize { + resolvedFont.isItalic = true + } if let pointSize { resolvedFont.pointSize = pointSize } @@ -212,6 +226,7 @@ public struct Font: Hashable, Sendable { public var lineHeight: Double public var weight: Weight public var design: Design + public var isItalic: Bool } public struct Context: Sendable { @@ -237,7 +252,8 @@ public struct Font: Hashable, Sendable { // constant ratio). lineHeight: (size * 1.25).rounded(.awayFromZero), weight: weight ?? .regular, - design: design ?? .default + design: design ?? .default, + isItalic: false ) } case .dynamic(let textStyle): @@ -248,7 +264,8 @@ public struct Font: Hashable, Sendable { pointSize: resolvedTextStyle.pointSize, lineHeight: resolvedTextStyle.lineHeight, weight: resolvedTextStyle.weight, - design: .default + design: .default, + isItalic: false ) } diff --git a/Sources/SwiftCrossUI/Views/Modifiers/Style/FontModifier.swift b/Sources/SwiftCrossUI/Views/Modifiers/Style/FontModifier.swift index aa0f8ce34a..b6eb15e100 100644 --- a/Sources/SwiftCrossUI/Views/Modifiers/Style/FontModifier.swift +++ b/Sources/SwiftCrossUI/Views/Modifiers/Style/FontModifier.swift @@ -52,4 +52,14 @@ extension View { ) } } + + /// Forces any contained text to become italic. + public func italic() -> some View { + EnvironmentModifier(self) { environment in + return environment.with( + \.fontOverlay.italicize, + true + ) + } + } } diff --git a/Sources/UIKitBackend/Font+UIFont.swift b/Sources/UIKitBackend/Font+UIFont.swift index ddd4066c5e..363e86b3e7 100644 --- a/Sources/UIKitBackend/Font+UIFont.swift +++ b/Sources/UIKitBackend/Font+UIFont.swift @@ -3,6 +3,7 @@ import UIKit extension Font.Resolved { var uiFont: UIFont { + let uiFont: UIFont switch identifier.kind { case .system: let weight: UIFont.Weight = @@ -29,10 +30,20 @@ extension Font.Resolved { switch design { case .monospaced: - return .monospacedSystemFont(ofSize: CGFloat(pointSize), weight: weight) + uiFont = .monospacedSystemFont(ofSize: CGFloat(pointSize), weight: weight) case .default: - return .systemFont(ofSize: CGFloat(pointSize), weight: weight) + uiFont = .systemFont(ofSize: CGFloat(pointSize), weight: weight) } } + + if isItalic { + let descriptor = uiFont.fontDescriptor.withSymbolicTraits(.traitItalic) + return UIFont( + descriptor: descriptor ?? uiFont.fontDescriptor, + size: CGFloat(pointSize) + ) + } else { + return uiFont + } } } diff --git a/Sources/WinUIBackend/WinUIBackend.swift b/Sources/WinUIBackend/WinUIBackend.swift index 30b3ecab03..3e7206c881 100644 --- a/Sources/WinUIBackend/WinUIBackend.swift +++ b/Sources/WinUIBackend/WinUIBackend.swift @@ -1752,6 +1752,9 @@ extension EnvironmentValues { control.fontWeight.weight = resolvedFont.winUIFontWeight control.foreground = winUIForegroundBrush control.isEnabled = isEnabled + if resolvedFont.isItalic { + control.fontStyle = .italic + } switch colorScheme { case .light: control.requestedTheme = .light @@ -1766,6 +1769,9 @@ extension EnvironmentValues { textBlock.fontSize = resolvedFont.pointSize textBlock.fontWeight.weight = resolvedFont.winUIFontWeight textBlock.foreground = winUIForegroundBrush + if resolvedFont.isItalic { + textBlock.fontStyle = .italic + } } }