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
22 changes: 11 additions & 11 deletions example/screens/testing-grounds/weather/WeatherTestingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { reloadWidgets, scheduleWidget, updateWidget, VoltraWidgetPreview, Widge

import { Button } from '~/components/Button'
import { Card } from '~/components/Card'
import { WeatherWidget } from '~/widgets/ios/IosWeatherWidget'
import { IosWeatherWidget } from '~/widgets/ios/IosWeatherWidget'
import { SAMPLE_WEATHER_DATA, type WeatherCondition, type WeatherData } from '~/widgets/weather-types'

const WIDGET_FAMILIES: { id: WidgetFamily; title: string; description: string }[] = [
Expand Down Expand Up @@ -74,9 +74,9 @@ export default function WeatherTestingScreen() {
setIsUpdating(true)
try {
await updateWidget('weather', {
systemSmall: <WeatherWidget weather={weatherData} />,
systemMedium: <WeatherWidget weather={weatherData} />,
systemLarge: <WeatherWidget weather={weatherData} />,
systemSmall: <IosWeatherWidget weather={weatherData} />,
systemMedium: <IosWeatherWidget weather={weatherData} />,
systemLarge: <IosWeatherWidget weather={weatherData} />,
})
await reloadWidgets(['weather'])
} catch (error) {
Expand Down Expand Up @@ -107,9 +107,9 @@ export default function WeatherTestingScreen() {
setIsUpdating(true)
try {
await updateWidget('weather', {
systemSmall: <WeatherWidget weather={customWeather} />,
systemMedium: <WeatherWidget weather={customWeather} />,
systemLarge: <WeatherWidget weather={customWeather} />,
systemSmall: <IosWeatherWidget weather={customWeather} />,
systemMedium: <IosWeatherWidget weather={customWeather} />,
systemLarge: <IosWeatherWidget weather={customWeather} />,
})
await reloadWidgets(['weather'])
} catch (error) {
Expand Down Expand Up @@ -237,9 +237,9 @@ export default function WeatherTestingScreen() {

try {
await updateWidget('weather', {
systemSmall: <WeatherWidget weather={weatherData} />,
systemMedium: <WeatherWidget weather={weatherData} />,
systemLarge: <WeatherWidget weather={weatherData} />,
systemSmall: <IosWeatherWidget weather={weatherData} />,
systemMedium: <IosWeatherWidget weather={weatherData} />,
systemLarge: <IosWeatherWidget weather={weatherData} />,
})
// Don't call reloadWidgets here to avoid resetting scheduled timelines
} catch (error) {
Expand Down Expand Up @@ -348,7 +348,7 @@ export default function WeatherTestingScreen() {
</Card.Text>
<View style={styles.previewContainer}>
<VoltraWidgetPreview family={selectedFamily} style={widgetPreviewStyle}>
<WeatherWidget weather={currentWeather} />
<IosWeatherWidget weather={currentWeather} />
</VoltraWidgetPreview>
</View>
</Card>
Expand Down
32 changes: 29 additions & 3 deletions packages/voltra/ios/target/VoltraHomeWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ public struct VoltraHomeWidgetProvider: TimelineProvider {
public struct VoltraHomeWidgetView: View {
public var entry: VoltraHomeWidgetEntry

@Environment(\.showsWidgetContainerBackground) private var showsWidgetContainerBackground
@Environment(\.widgetRenderingMode) private var widgetRenderingMode

public init(entry: VoltraHomeWidgetEntry) {
self.entry = entry
}
Expand All @@ -285,12 +288,22 @@ public struct VoltraHomeWidgetView: View {
}

public var body: some View {
let mappedRenderingMode = mapWidgetRenderingMode(widgetRenderingMode)

Group {
if let root = entry.rootNode {
// No parsing here - just render the pre-parsed AST
let content = Voltra(root: root, activityId: "widget")
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.widgetURL(resolveDeepLinkURL(entry))
let content = Voltra(
root: root,
activityId: "widget",
widget: VoltraWidgetEnvironment(
isHomeScreenWidget: true,
renderingMode: mappedRenderingMode,
showsContainerBackground: showsWidgetContainerBackground
)
)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.widgetURL(resolveDeepLinkURL(entry))

if showRefreshButton {
content.overlay(alignment: .topTrailing) {
Expand All @@ -306,6 +319,19 @@ public struct VoltraHomeWidgetView: View {
.disableWidgetMarginsIfAvailable()
}

private func mapWidgetRenderingMode(_ mode: WidgetRenderingMode) -> VoltraWidgetRenderingMode {
switch mode {
case .fullColor:
return .fullColor
case .accented:
return .accented
case .vibrant:
return .vibrant
default:
return .unknown
}
}

@ViewBuilder
private var refreshButton: some View {
if #available(iOSApplicationExtension 17.0, *) {
Expand Down
2 changes: 1 addition & 1 deletion packages/voltra/ios/ui/Layout/FlexContainerHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ struct FlexContainerStyleModifier: ViewModifier {

content
.modifier(LayoutModifier(style: layoutWithoutPadding))
.modifier(DecorationModifier(style: values.decoration))
.modifier(DecorationModifier(style: values.decoration, layout: layout))
.modifier(RenderingModifier(style: values.rendering))
.voltraIfLet(layout.margin) { c, margin in
c.background(.clear).padding(margin)
Expand Down
4 changes: 2 additions & 2 deletions packages/voltra/ios/ui/Style/CompositeStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct CompositeStyleModifier: ViewModifier {
content
.voltraIfLet(layout.padding) { c, p in c.padding(p) }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: contentAlignment)
.modifier(DecorationModifier(style: decoration))
.modifier(DecorationModifier(style: decoration, layout: layout))
.modifier(RenderingModifier(style: rendering))
.layoutValue(key: FlexItemLayoutKey.self, value: FlexItemValues(
flexGrow: layout.flexGrow,
Expand Down Expand Up @@ -53,7 +53,7 @@ struct CompositeStyleModifier: ViewModifier {
alignment: alignment
)
}
.modifier(DecorationModifier(style: decoration))
.modifier(DecorationModifier(style: decoration, layout: layout))
.modifier(RenderingModifier(style: rendering))
}
}
Expand Down
40 changes: 38 additions & 2 deletions packages/voltra/ios/ui/Style/DecorationStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ struct DecorationStyle {

struct DecorationModifier: ViewModifier {
let style: DecorationStyle
let layout: LayoutStyle?
@Environment(\.voltraEnvironment) private var voltraEnvironment

private func point(from unitPoint: UnitPoint, in size: CGSize) -> CGPoint {
CGPoint(x: unitPoint.x * size.width, y: unitPoint.y * size.height)
Expand Down Expand Up @@ -105,9 +107,43 @@ struct DecorationModifier: ViewModifier {
.allowsHitTesting(false)
}

private var suppressesDecorativeContainerEffects: Bool {
voltraEnvironment.widget?.suppressesDecorativeContainerEffects == true
}

private var isFullBleedBackgroundCandidate: Bool {
guard let layout else { return false }

if let flex = layout.flex, flex > 0 {
return true
}

if layout.flexGrow > 0 {
return true
}

return layout.width == .fill && layout.height == .fill
}

private var resolvedBackgroundColor: BackgroundValue? {
guard suppressesDecorativeContainerEffects, isFullBleedBackgroundCandidate else {
return style.backgroundColor
}

return nil
}

private var resolvedGlassEffect: GlassEffect? {
guard suppressesDecorativeContainerEffects else {
return style.glassEffect
}

return nil
}

func body(content: Content) -> some View {
content
.voltraIfLet(style.backgroundColor) { content, bg in
.voltraIfLet(resolvedBackgroundColor) { content, bg in
switch bg {
case let .color(color):
content.background(color)
Expand Down Expand Up @@ -151,7 +187,7 @@ struct DecorationModifier: ViewModifier {
y: shadow.offset.height
)
}
.voltraIfLet(style.glassEffect) { content, glassEffect in
.voltraIfLet(resolvedGlassEffect) { content, glassEffect in
if #available(iOS 26.0, *) {
switch glassEffect {
case .clear:
Expand Down
11 changes: 10 additions & 1 deletion packages/voltra/ios/ui/Style/TextStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ struct TextStyle {

struct TextStyleModifier: ViewModifier {
let style: TextStyle
@Environment(\.voltraEnvironment) private var voltraEnvironment

private var resolvedColor: Color {
if let widget = voltraEnvironment.widget, widget.usesReducedBackgroundPresentation {
return .primary
}

return style.color
}

func body(content: Content) -> some View {
content
Expand All @@ -28,7 +37,7 @@ struct TextStyleModifier: ViewModifier {
: .system(size: style.fontSize, weight: style.fontWeight)
)
// 2. Color
.foregroundColor(style.color)
.foregroundColor(resolvedColor)
// 3. Layout / Spacing
.multilineTextAlignment(style.alignment)
.lineLimit(style.lineLimit)
Expand Down
5 changes: 4 additions & 1 deletion packages/voltra/ios/ui/Views/VoltraGlassContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ public struct VoltraGlassContainer: VoltraView {
public typealias Parameters = GlassContainerParameters

public let element: VoltraElement
@Environment(\.voltraEnvironment) private var voltraEnvironment

public init(_ element: VoltraElement) {
self.element = element
}

public var body: some View {
if let children = element.children {
if #available(iOS 26.0, *) {
if voltraEnvironment.widget?.suppressesDecorativeContainerEffects == true {
children.applyStyle(element.style)
} else if #available(iOS 26.0, *) {
let spacing = params.spacing ?? 0.0
GlassEffectContainer(spacing: CGFloat(spacing)) {
children
Expand Down
45 changes: 37 additions & 8 deletions packages/voltra/ios/ui/Views/VoltraLinearGradient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public struct VoltraLinearGradient: VoltraView {
public typealias Parameters = LinearGradientParameters

public let element: VoltraElement
@Environment(\.voltraEnvironment) private var voltraEnvironment

public init(_ element: VoltraElement) {
self.element = element
Expand Down Expand Up @@ -67,20 +68,48 @@ public struct VoltraLinearGradient: VoltraView {
return Gradient(colors: [Color.black.opacity(0.25), Color.black.opacity(0.05)])
}

private func isFullBleedWidgetBackgroundCandidate() -> Bool {
guard let style = element.style, element.children != nil else {
return false
}
if let flex = style["flex"]?.doubleValue, flex > 0 {
return true
}
if let flexGrow = style["flexGrow"]?.doubleValue, flexGrow > 0 {
return true
}
let width = style["width"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
let height = style["height"]?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines)
return width == "100%" && height == "100%"
}

public var body: some View {
let gradient = buildGradient(params: params)
let start = parsePoint(params.startPoint)
let end = parsePoint(params.endPoint)
let anyStyle = element.style?.mapValues { $0.toAny() } ?? [:]
let (layout, baseDecoration, rendering, text) = StyleConverter.convert(anyStyle)

var decoration = baseDecoration
decoration.backgroundColor = .linearGradient(gradient: gradient, startPoint: start, endPoint: end)

// Note: dither parameter is available in node.parameters["dither"] but SwiftUI's LinearGradient
// doesn't expose dithering control directly. This is handled automatically by the system.
let lg = LinearGradient(gradient: gradient, startPoint: start, endPoint: end)
if let widget = voltraEnvironment.widget,
widget.isHomeScreenWidget,
widget.usesReducedBackgroundPresentation,
isFullBleedWidgetBackgroundCandidate(),
let children = element.children
{
return AnyView(children.applyStyle(element.style))
}

// Use ZStack with a Rectangle that fills and is tinted by the gradient, then overlay children.
return ZStack {
Rectangle().fill(lg)
element.children ?? .empty
if let children = element.children {
return AnyView(
children.applyStyle((layout, decoration, rendering, text))
)
}
.applyStyle(element.style)

return AnyView(
Color.clear.applyStyle((layout, decoration, rendering, text))
)
}
}
11 changes: 10 additions & 1 deletion packages/voltra/ios/ui/Views/VoltraText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ public struct VoltraText: VoltraView {
public typealias Parameters = TextParameters

public let element: VoltraElement
@Environment(\.voltraEnvironment) private var voltraEnvironment

public init(_ element: VoltraElement) {
self.element = element
Expand Down Expand Up @@ -58,13 +59,21 @@ public struct VoltraText: VoltraView {
return textStyle.alignment
}()

let resolvedColor: Color = {
if let widget = voltraEnvironment.widget, widget.usesReducedBackgroundPresentation {
return .primary
}

return textStyle.color
}()

Text(.init(textContent))
.kerning(textStyle.letterSpacing)
.underline(textStyle.decoration == .underline || textStyle.decoration == .underlineLineThrough)
.strikethrough(textStyle.decoration == .lineThrough || textStyle.decoration == .underlineLineThrough)
// These technically work on View, but good to keep close
.font(font)
.foregroundColor(textStyle.color)
.foregroundColor(resolvedColor)
.multilineTextAlignment(alignment)
.lineSpacing(textStyle.lineSpacing)
.voltraIfLet(params.numberOfLines) { view, numberOfLines in
Expand Down
Loading