From 33e237eb6779ecfda70c781caa656827ee9d70f8 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 17 May 2026 11:05:43 -0700
Subject: [PATCH] visionos: persist all configurable window settings
Save the gaze indicator and dimmed mode setting as well as curvature
---
include/SDL3/SDL_events.h | 4 +-
include/SDL3/SDL_video.h | 15 +---
src/events/SDL_events.c | 2 +-
.../uikit/SDL_CurvedContentHosting.swift | 82 ++++++++++++++++---
src/video/uikit/SDL_UIKitBridge-swift.h | 8 +-
src/video/uikit/SDL_UIKitBridge.m | 18 ++--
src/video/uikit/SDL_uikitviewcontroller.m | 2 +-
src/video/uikit/SDL_uikitwindow.h | 2 +-
src/video/uikit/SDL_uikitwindow.m | 13 +--
9 files changed, 98 insertions(+), 48 deletions(-)
diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index dd822ecc71936..0a3827571f342 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -163,9 +163,9 @@ typedef enum SDL_EventType
associated with the window. Otherwise, the handle has already been destroyed and all resources
associated with it are invalid */
SDL_EVENT_WINDOW_HDR_STATE_CHANGED, /**< Window HDR properties have changed */
- SDL_EVENT_WINDOW_CURVATURE_CHANGED, /**< Window curvature has changed to data1 (on visionOS) */
+ SDL_EVENT_WINDOW_SETTINGS_CHANGED, /**< Window settings have changed (on visionOS) */
SDL_EVENT_WINDOW_FIRST = SDL_EVENT_WINDOW_SHOWN,
- SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_CURVATURE_CHANGED,
+ SDL_EVENT_WINDOW_LAST = SDL_EVENT_WINDOW_SETTINGS_CHANGED,
/* Keyboard events */
SDL_EVENT_KEY_DOWN = 0x300, /**< Key pressed */
diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index 821c176101178..1eeb7db6cda93 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -1386,11 +1386,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreatePopupWindow(SDL_Window *paren
*
* These are additional supported properties with visionOS:
*
- * - `SDL_PROP_WINDOW_CREATE_CURVATURE_FLOAT`: the curvature of the window on
- * visionOS. Curved windows have square corners and additional controls for
- * more immersive gaming. This can be -1 (disabled), which is the default, 0
- * (no curve), or set to a specific curvature radius in millimeters. A
- * common value for a gaming monitor is 1000.
+ * - `SDL_PROP_WINDOW_CREATE_VISIONOS_SETTINGS_STRING`: the settings of the window in JSON format. If this isn't set, the window will have standard UIKit behavior. If this is set to "" or a valid setting string then the window is created with enhanced features allowing curved display. The curvature in the settings is defined as a radius in millimeters. A common value for a gaming monitor is 1000 and a setting string for that would be "{\"curvatureRadius\":1000}".
*
* If this window is being created to be used with an SDL_Renderer, you should
* not add a graphics API specific property
@@ -1454,7 +1450,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_CreateWindowWithProperties(SDL_Prop
#define SDL_PROP_WINDOW_CREATE_X11_WINDOW_NUMBER "SDL.window.create.x11.window"
#define SDL_PROP_WINDOW_CREATE_EMSCRIPTEN_CANVAS_ID_STRING "SDL.window.create.emscripten.canvas_id"
#define SDL_PROP_WINDOW_CREATE_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING "SDL.window.create.emscripten.keyboard_element"
-#define SDL_PROP_WINDOW_CREATE_CURVATURE_FLOAT "SDL.window.create.curvature"
+#define SDL_PROP_WINDOW_CREATE_VISIONOS_SETTINGS_STRING "SDL.window.create.visionos.settings"
/**
* Get the numeric ID of a window.
@@ -1635,10 +1631,7 @@ extern SDL_DECLSPEC SDL_Window * SDLCALL SDL_GetWindowParent(SDL_Window *window)
*
* On visionOS:
*
- * - `SDL_PROP_WINDOW_CURVATURE_FLOAT`: the curvature of the window in curved
- * mode on visionOS. This value is updated dynamically when changed via the
- * screen ornaments. This can be 0 (no curve), or a specific curvature
- * radius in millimeters. A common value for a gaming monitor is 1000.
+ * - `SDL_PROP_WINDOW_VISIONOS_SETTINGS_STRING`: the current settings of the window in JSON format, or NULL if the window has standard UIKit behavior. SDL_EVENT_WINDOW_SETTINGS_CHANGED is sent when this value changes.
*
* \param window the window to query.
* \returns a valid property ID on success or 0 on failure; call
@@ -1689,7 +1682,7 @@ extern SDL_DECLSPEC SDL_PropertiesID SDLCALL SDL_GetWindowProperties(SDL_Window
#define SDL_PROP_WINDOW_X11_WINDOW_NUMBER "SDL.window.x11.window"
#define SDL_PROP_WINDOW_EMSCRIPTEN_CANVAS_ID_STRING "SDL.window.emscripten.canvas_id"
#define SDL_PROP_WINDOW_EMSCRIPTEN_KEYBOARD_ELEMENT_STRING "SDL.window.emscripten.keyboard_element"
-#define SDL_PROP_WINDOW_CURVATURE_FLOAT "SDL.window.curvature"
+#define SDL_PROP_WINDOW_VISIONOS_SETTINGS_STRING "SDL.window.visionos.settings"
/**
* Get the window flags.
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index 19f5a85adc95d..e2ef1a35f3486 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -565,7 +565,7 @@ int SDL_GetEventDescription(const SDL_Event *event, char *buf, int buflen)
SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_LEAVE_FULLSCREEN);
SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_DESTROYED);
SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_HDR_STATE_CHANGED);
- SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_CURVATURE_CHANGED);
+ SDL_WINDOWEVENT_CASE(SDL_EVENT_WINDOW_SETTINGS_CHANGED);
#undef SDL_WINDOWEVENT_CASE
#define PRINT_KEYDEV_EVENT(event) (void)SDL_snprintf(details, sizeof(details), " (timestamp=%" SDL_PRIu64 " which=%u)", event->kdevice.timestamp, (uint)event->kdevice.which)
diff --git a/src/video/uikit/SDL_CurvedContentHosting.swift b/src/video/uikit/SDL_CurvedContentHosting.swift
index d88b43680e14e..d8dccaf3bdd33 100644
--- a/src/video/uikit/SDL_CurvedContentHosting.swift
+++ b/src/video/uikit/SDL_CurvedContentHosting.swift
@@ -93,7 +93,7 @@ internal class SDL_ClearHostingController<Content: View>: UIHostingController<Co
@MainActor
@objc(SDL_CurvedContentHosting)
internal class SDL_CurvedContentHosting: NSObject {
- private let settings = SDL_CurvedContentSettings()
+ private let settings = SDL_CurvedContentSettings.load()
private let helper = SDL_RealityKitHelper()
@@ -127,7 +127,7 @@ internal class SDL_CurvedContentHosting: NSObject {
// Spin up an async task to present / dismiss ornaments when there are updates to the scene state.
let settings = self.settings
let sceneStateObservations = Observations { [weak settings] in
- guard let settings else { return nil as (SDL_CurvedContentSettings.SceneState, SDL_CurvedContentSettings.InputType, Bool, Bool)? }
+ guard let settings else { return nil as (SceneState, InputType, Bool, Bool)? }
return (settings.sceneState, settings.inputType, settings.isSnapped, settings.settingsExpanded)
}
Task { [weak self] in
@@ -195,26 +195,76 @@ internal class SDL_CurvedContentHosting: NSObject {
// MARK: - Settings Panel
+/// State of the app user interface, determined by the content view's state.
+enum SceneState: String, Codable {
+ /// A state which allows the user to configure the scene. Ornaments should be visible.
+ case interactive
+
+ /// A state which hides all UI except for the game itself. Ornaments should not be visible.
+ case cinematic
+}
+
+enum InputType: String, Codable {
+ case eyes
+ case pointer
+}
+
+internal class SDL_CurvedContentPersistentSettings: Codable {
+ var inputType: InputType?
+ var showHover: Bool?
+ var isDimmed: Bool?
+ var curvatureRadius: Float?
+}
+
@Observable
internal class SDL_CurvedContentSettings {
- /// State of the app user interface, determined by the content view's state.
- enum SceneState {
- /// A state which allows the user to configure the scene. Ornaments should be visible.
- case interactive
- /// A state which hides all UI except for the game itself. Ornaments should not be visible.
- case cinematic
+ static func load() -> SDL_CurvedContentSettings {
+ let settings = SDL_CurvedContentSettings()
+ if let json = SDL_VisionOS_GetWindowSettings() {
+ if json != "", let data = json.data(using: .utf8) {
+ do {
+ let values = try JSONDecoder().decode(SDL_CurvedContentPersistentSettings.self, from:data)
+ if let inputType = values.inputType {
+ settings.inputType = inputType
+ }
+ if let showHover = values.showHover {
+ settings.showHover = showHover
+ }
+ if let isDimmed = values.isDimmed {
+ settings.isDimmed = isDimmed
+ }
+ if let curvatureRadius = values.curvatureRadius {
+ settings.curvatureRadius = curvatureRadius
+ }
+ } catch {
+ NSLog("Couldn't parse window settings: %@", error.localizedDescription)
+ }
+ }
+ }
+ return settings
}
- enum InputType {
- case eyes
- case pointer
+ func save() {
+ let values = SDL_CurvedContentPersistentSettings()
+ values.inputType = inputType
+ values.showHover = showHover
+ values.isDimmed = isDimmed
+ values.curvatureRadius = curvatureRadius
+
+ do {
+ let data = try JSONEncoder().encode(values)
+ let json = String(data: data, encoding: String.Encoding.utf8)
+ SDL_VisionOS_SendWindowSettings(json)
+ } catch {
+ NSLog("Couldn't encode window settings: %@", error.localizedDescription)
+ }
}
var inputType: InputType = .eyes
var showHover: Bool = true
var isDimmed: Bool = false
- var curvatureRadius: Float = SDL_VisionOS_GetCurvature()
+ var curvatureRadius: Float = 0.0
var sceneState: SceneState = .interactive
var isSnapped: Bool = false
var settingsExpanded: Bool = false
@@ -330,6 +380,9 @@ struct SDL_SettingsPanelView: View {
Toggle(isOn: $settings.showHover) {
}
+ .onChange(of: settings.showHover) {
+ settings.save()
+ }
.labelsHidden()
.tint(.secondary)
@@ -341,6 +394,9 @@ struct SDL_SettingsPanelView: View {
Toggle(isOn: $settings.isDimmed) {
}
+ .onChange(of: settings.isDimmed) {
+ settings.save()
+ }
.labelsHidden()
.tint(.secondary)
@@ -383,7 +439,7 @@ struct SDL_SettingsPanelView: View {
+ (1.0 - curvatureSlider) * Self.maximumCurvatureRadius)
settings.curvatureRadius = radius
}
- SDL_VisionOS_SendCurvatureChanged(settings.curvatureRadius)
+ settings.save()
}
CurviestButtonIcon()
diff --git a/src/video/uikit/SDL_UIKitBridge-swift.h b/src/video/uikit/SDL_UIKitBridge-swift.h
index e63dc6c42bbb9..a59414b4d32ec 100644
--- a/src/video/uikit/SDL_UIKitBridge-swift.h
+++ b/src/video/uikit/SDL_UIKitBridge-swift.h
@@ -23,11 +23,11 @@
// Called from Swift scene delegates when window size changes
void SDL_VisionOS_SendSizeChanged(long width, long height);
-// Called from Swift scene delegates to get the initial curvature
-float SDL_VisionOS_GetCurvature();
+// Called from Swift scene delegates to get the initial window settings
+NSString *SDL_VisionOS_GetWindowSettings();
-// Called from Swift scene delegates when window curvature changes
-void SDL_VisionOS_SendCurvatureChanged(float curvature);
+// Called from Swift scene delegates when window settings change
+void SDL_VisionOS_SendWindowSettings(NSString *settings);
// Called from Swift scene delegates when pointer mode changes
void SDL_VisionOS_SendPointerMode(bool enabled);
diff --git a/src/video/uikit/SDL_UIKitBridge.m b/src/video/uikit/SDL_UIKitBridge.m
index b5c552f51a727..e869d4b22c856 100644
--- a/src/video/uikit/SDL_UIKitBridge.m
+++ b/src/video/uikit/SDL_UIKitBridge.m
@@ -57,27 +57,27 @@ void SDL_VisionOS_SendSizeChanged(long width, long height)
}
}
-// Called from Swift scene delegates to get the initial curvature
-float SDL_VisionOS_GetCurvature()
+// Called from Swift scene delegates to get the initial window settings
+NSString *SDL_VisionOS_GetWindowSettings()
{
SDL_Window *window = SDL_GetToplevelForKeyboardFocus();
if (window) {
SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
- return data.curvature;
+ return data.settings;
}
- return 0.0f;
+ return nil;
}
// Called from Swift scene delegates when window curvature changes
-void SDL_VisionOS_SendCurvatureChanged(float curvature)
+void SDL_VisionOS_SendWindowSettings(NSString *settings)
{
SDL_Window *window = SDL_GetToplevelForKeyboardFocus();
if (window) {
SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->internal;
- if (curvature != data.curvature) {
- data.curvature = curvature;
- SDL_SetFloatProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_CURVATURE_FLOAT, curvature);
- SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_CURVATURE_CHANGED, (int)curvature, 0);
+ if (![settings isEqualToString:data.settings]) {
+ data.settings = settings;
+ SDL_SetStringProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_VISIONOS_SETTINGS_STRING, settings.UTF8String);
+ SDL_SendWindowEvent(window, SDL_EVENT_WINDOW_SETTINGS_CHANGED, 0, 0);
}
}
}
diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m
index dc776741d98f0..7da8731699717 100644
--- a/src/video/uikit/SDL_uikitviewcontroller.m
+++ b/src/video/uikit/SDL_uikitviewcontroller.m
@@ -127,7 +127,7 @@ - (instancetype)initWithSDLWindow:(SDL_Window *)_window
#ifdef SDL_PLATFORM_VISIONOS
if (@available(visionOS 26.0, *)) {
SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)self.window->internal;
- if (data.curvature >= 0.0f) {
+ if (data.settings != nil) {
[self initializeVisionOSCurvedUI];
}
}
diff --git a/src/video/uikit/SDL_uikitwindow.h b/src/video/uikit/SDL_uikitwindow.h
index b9077ad36d825..42a531cd65ccc 100644
--- a/src/video/uikit/SDL_uikitwindow.h
+++ b/src/video/uikit/SDL_uikitwindow.h
@@ -55,7 +55,7 @@ extern NSUInteger UIKit_GetSupportedOrientations(SDL_Window *window);
#ifdef SDL_PLATFORM_VISIONOS
// Hosting controller for curved content mode (UIHostingController-based)
@property(nonatomic, strong) id curvedContentHosting;
-@property(nonatomic, assign) CGFloat curvature;
+@property(nonatomic, strong) NSString *settings;
#endif
@end
diff --git a/src/video/uikit/SDL_uikitwindow.m b/src/video/uikit/SDL_uikitwindow.m
index 3c3b4fd926e99..5523d6c55f07e 100644
--- a/src/video/uikit/SDL_uikitwindow.m
+++ b/src/video/uikit/SDL_uikitwindow.m
@@ -106,18 +106,19 @@ static bool SetupWindowData(SDL_VideoDevice *_this, SDL_Window *window, UIWindow
#endif
window->w = width;
window->h = height;
-
+
SDL_PropertiesID props = SDL_GetWindowProperties(window);
SDL_SetPointerProperty(props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, (__bridge void *)data.uiwindow);
SDL_SetNumberProperty(props, SDL_PROP_WINDOW_UIKIT_METAL_VIEW_TAG_NUMBER, SDL_METALVIEW_TAG);
#ifdef SDL_PLATFORM_VISIONOS
- float curvature = SDL_GetFloatProperty(create_props, SDL_PROP_WINDOW_CREATE_CURVATURE_FLOAT, -1.0f);
- if (curvature > 0.0f && curvature <= 1.0f) {
- curvature = 0.0f;
+ const char *settings = SDL_GetStringProperty(create_props, SDL_PROP_WINDOW_CREATE_VISIONOS_SETTINGS_STRING, NULL);
+ if (settings) {
+ data.settings = [NSString stringWithUTF8String:settings];
+ } else {
+ data.settings = nil;
}
- data.curvature = curvature;
- SDL_SetFloatProperty(props, SDL_PROP_WINDOW_CURVATURE_FLOAT, curvature);
+ SDL_SetStringProperty(props, SDL_PROP_WINDOW_VISIONOS_SETTINGS_STRING, settings);
#endif
/* The View Controller will handle rotating the view when the device