Skip to content

[FEAT] LinearGradient, RadialGradient & AngularGradient#450

Open
MiaKoring wants to merge 28 commits intomoreSwift:mainfrom
MiaKoring:feat/gradient
Open

[FEAT] LinearGradient, RadialGradient & AngularGradient#450
MiaKoring wants to merge 28 commits intomoreSwift:mainfrom
MiaKoring:feat/gradient

Conversation

@MiaKoring
Copy link
Copy Markdown
Collaborator

@MiaKoring MiaKoring commented Feb 17, 2026

Summary

Added support for 3 types of gradients to be rendered as Views:

  • LinearGradient
  • RadialGradient
  • AngularGradient

Changes

SwiftCrossUI

  • added functions for creation and update of each gradient type to the AppBackend protocol.
  • added an ElementaryViewstruct for each type of gradient.
  • added a struct Angle intended for use by AngularGradient later
    • gets also used by multiple backends for calculations
    • added an initializer to create an Angle from a UnitPoint
  • added a struct UnitPoint, meant to represent a normalized point in a view’s coordinate space
    • added static values for various points on a rectangle
    • AppKitBackend views using it may need to flip the coordinates vertically, due to AppKit using a different coordinate origin point than the other major platforms.
    • UnitPoints are not clamped, so you can use them with arbitrary values to benefit for example from Angle(origin:destination:)
  • added a struct Gradient, used by all implementations of a Gradient, its a gradient type agnostic representation of a color gradient
    • added nested struct Gradient.Stop representing a color and its normalized position in the gradient
    • added initializers taking [Color] or [Gradient.Stop]

AppKitBackend, UIKitBackend

  • Added implementations of the new AppBackend methods
  • Added separate Widget classes when needed (AppKit & UIKit)

WinUI, GtkBackend

  • Added implementation of the new AppBackend methods related to LinearGradient and RadialGradient

SwiftCrossUITests

  • Added Tests validating the initializers of Gradient
    • even distribution of stops when supplied [Color]
    • transparent stops when passed an empty array of Color (to make future support of addition possible and remove edgecases to check outside of Gradient)
    • start and end stop of the same color when passed [Color] with one entry
    • preservation of the order the colors are passed

Examples

  • Added GradientsExample
    • added tab with examples for LinearGradient
    • added tab with examples for RadialGradient
    • added tab with examples for AngularGradient

Notes

  • WinUI 2 and 3 don’t support conic (angular) gradients yet. I currently don’t know what an alternative could be
  • This PR only adds support for rendering them as views directly. A future ShapeStyle could easily re-use the added structs tho.

TODO / Status

  • Added RadialGradient support to WinUIBackend (pending RadialGradientBrush generation)
  • “unavailable on WinUIBackend” documentation comment is removed from RadialGradient

@MiaKoring
Copy link
Copy Markdown
Collaborator Author

MiaKoring commented Feb 17, 2026

Screenshot 2026-02-17 at 20 29 46 Screenshot 2026-02-17 at 20 29 57 Screenshot 2026-02-18 at 12 38 59 Screenshot 2026-02-17 at 20 30 46 Screenshot 2026-02-17 at 20 30 54 Screenshot 2026-02-18 at 12 39 07

@MiaKoring
Copy link
Copy Markdown
Collaborator Author

Related issue: #427

@MiaKoring
Copy link
Copy Markdown
Collaborator Author

The new commit now offers full API parity to SwiftUI on AngularGradient and correctly renders all 6 test gradients. “Correct” meaning looking like the SwiftUI rendered result.

All 3 supported frameworks recieved this change and the shared screenshots were updated.

@MiaKoring MiaKoring marked this pull request as ready for review February 18, 2026 15:53
@MiaKoring
Copy link
Copy Markdown
Collaborator Author

Outstanding Tasks have been completed, this can now be reviewed and merged

Copy link
Copy Markdown
Collaborator

@stackotter stackotter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this feature! I've reviewed all of the code now, but I haven't tested anything locally yet. I plan to construct a SwiftUI app with a bunch of edge cases that I have thought of, and then compile that with your PR and ensure that all of the edge cases act the same across platforms. It should be a useful tool to help you address some of my PR comments too. I likely won't get around to doing that today though.

Comment thread Examples/Sources/GradientsExample/GradientsApp.swift Outdated
Comment thread Examples/Sources/GradientsExample/GradientsApp.swift Outdated
Comment thread Examples/Sources/GradientsExample/GradientsApp.swift Outdated
Comment thread Examples/Sources/GradientsExample/GradientsApp.swift Outdated
Comment thread Examples/Sources/GradientsExample/GradientsApp.swift Outdated
Comment thread Sources/GtkBackend/GtkBackend+Gradient.swift Outdated
Comment thread Sources/GtkBackend/GtkBackend+Gradient.swift Outdated
Comment thread Sources/AppKitBackend/AppKitBackend+Gradient.swift Outdated
Comment thread Sources/UIKitBackend/UIKitBackend+Gradient.swift Outdated
Comment thread Sources/UIKitBackend/UIKitBackend+Gradient.swift
@MiaKoring
Copy link
Copy Markdown
Collaborator Author

conflicts should be resolved now, same for all requested changes without open discussion

Copy link
Copy Markdown
Collaborator

@stackotter stackotter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for applying all that feedback! I've got a few more things, but I think most of them are relatively small things this time (organisation, documentation, etc). I don't have time to test things locally right now, but I will try to do that soon, because I may be able to resolve some of my own questions and comments by testing out the edge cases that I'm worried about.

Comment thread Examples/Sources/GradientsExample/GradientsApp.swift
Comment thread Examples/Sources/GradientsExample/GradientsApp.swift Outdated
Comment thread Examples/Sources/GradientsExample/GradientsApp.swift Outdated
Comment thread Examples/Sources/GradientsExample/GradientsApp.swift Outdated
Comment thread Examples/Package.swift Outdated
Comment thread Sources/SwiftCrossUI/Views/Gradients/RadialGradient.swift Outdated

#if DEBUG
if range < 0 {
logger.warning(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's easy enough for us to support right? Because we can just automatically reverse the gradient. Use cases where it's probably most applicable would be graphical editors where the user is entering the radii with draggable handles or something similar. I assume some backends that don't use adjustedStops may already support having the radii swapped?

Comment thread Sources/SwiftCrossUI/Views/Gradients/RadialGradient.swift Outdated
) {
let widget = widget as! GradientView
widget.setGradientLayer(
to: CAGradientLayer.angularGradientLayer(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the stretching would also effect the angles of the radial lines of equal colour in conic gradients? My assumption is that SwiftUI doesn't do that, but maybe it does

Comment thread Sources/GtkBackend/GtkBackend+Gradient.swift Outdated
@MiaKoring
Copy link
Copy Markdown
Collaborator Author

GtkBackend will not support the AngularGradient type right now due to rendering inconsistencies with SwiftUI and the Apple Backends which are not easily resolvable.

For future reference if needed:

public func createAngularGradient() -> Widget {
    Box()
}

public func updateAngularGradient(
    _ widget: Widget,
    gradient: AngularGradient,
    withSize size: SIMD2<Int>,
    in environment: EnvironmentValues
) {
    let widget = widget as! Box

    let adjustedStops = gradient.adjustedStops

    let stops = adjustedStops.map { stop in
        let resolved = stop.color.resolve(in: environment)
        let red = resolved.red * 255
        let green = resolved.green * 255
        let blue = resolved.blue * 255
        
        let location = stop.location * 360
        
        return "rgba(\(red), \(green), \(blue), \(resolved.opacity)) \(location)deg”
    }.joined(separator: ",)
    
    let startDegrees = gradient.startAngle.degrees + 90
    let centerXPercent = gradient.center.x * 100
    let centerYPercent = gradient.center.y * 100

    widget.css.set(
        property: .init(
            key: “background”,
            value: “””
                conic-gradient(from \(startDegrees)deg \
                at \(centerXPercent)% \(centerYPercent)%, \
                \(stops))
                “””
        )
    )
}

@MiaKoring
Copy link
Copy Markdown
Collaborator Author

I believe I’m caught up. main is merged again too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants