visionos: persist all configurable window settings

Save the gaze indicator and dimmed mode setting as well as curvature
This commit is contained in:
Sam Lantinga
2026-05-17 11:05:43 -07:00
parent 6d3404e4bb
commit 33e237eb67
9 changed files with 98 additions and 48 deletions

View File

@@ -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 */

View File

@@ -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.

View File

@@ -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)

View File

@@ -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()

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -127,7 +127,7 @@ static void SDLCALL SDL_HideHomeIndicatorHintChanged(void *userdata, const char
#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];
}
}

View File

@@ -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

View File

@@ -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