yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
ios_window_backend.mm
Go to the documentation of this file.
2
3#if defined(__APPLE__)
4#include <TargetConditionals.h>
5#endif
6
7#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
8#import <CoreFoundation/CoreFoundation.h>
9#import <Metal/Metal.h>
10#import <MetalKit/MetalKit.h>
11#import <UIKit/UIKit.h>
12#endif
13
14#include <algorithm>
15#include <cmath>
16#include <string>
17
21#include "app/gui/core/style.h"
24#include "imgui/backends/imgui_impl_metal.h"
25#include "imgui/imgui.h"
26#include "imgui/imgui_internal.h"
27#include "util/log.h"
28#include "util/platform_paths.h"
29
30namespace yaze {
31namespace platform {
32
33namespace {
34#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
35UIEdgeInsets GetSafeAreaInsets(MTKView* view) {
36 if (!view) {
37 return UIEdgeInsetsZero;
38 }
39 if (@available(iOS 11.0, *)) {
40 return view.safeAreaInsets;
41 }
42 return UIEdgeInsetsZero;
43}
44
45// Apply touch-friendly ImGui style. Widget sizing is computed once and only
46// updated when the touch scale changes. Safe area padding is updated per-frame
47// but only when the values actually change, preventing layout oscillation.
48void ApplyTouchStyle(MTKView* view) {
49 struct TouchStyleState {
50 bool initialized = false;
51 float last_scale = 0.0f;
52 float last_safe_x = -1.0f;
53 float last_safe_y = -1.0f;
54 // Baselines captured from the original ImGui style
55 ImVec2 touch_extra = ImVec2(0.0f, 0.0f);
56 ImVec2 frame_padding = ImVec2(0.0f, 0.0f);
57 ImVec2 item_spacing = ImVec2(0.0f, 0.0f);
58 float scrollbar_size = 0.0f;
59 float grab_min_size = 0.0f;
60 };
61
62 static TouchStyleState state;
63
64 ImGuiStyle& style = ImGui::GetStyle();
65 ImGuiIO& io = ImGui::GetIO();
66 io.ConfigWindowsMoveFromTitleBarOnly = true;
67 io.ConfigWindowsResizeFromEdges = false;
68
69 if (!state.initialized) {
70 state.touch_extra = style.TouchExtraPadding;
71 state.frame_padding = style.FramePadding;
72 state.item_spacing = style.ItemSpacing;
73 state.scrollbar_size = style.ScrollbarSize;
74 state.grab_min_size = style.GrabMinSize;
75 state.initialized = true;
76 }
77
78 float scale = std::clamp(ios::GetTouchScale(), 0.75f, 1.6f);
79
80 // Only recompute widget sizing when the touch scale actually changes.
81 if (scale != state.last_scale) {
82 state.last_scale = scale;
83
84 const float frame_height = ImGui::GetFrameHeight();
85 const float target_height = std::max(44.0f * scale, frame_height);
86 const float touch_extra =
87 std::clamp((target_height - frame_height) * 0.5f, 0.0f, 16.0f * scale);
88 style.TouchExtraPadding =
89 ImVec2(std::max(state.touch_extra.x * scale, touch_extra),
90 std::max(state.touch_extra.y * scale, touch_extra));
91
92 const float font_size = ImGui::GetFontSize();
93 if (font_size > 0.0f) {
94 style.ScrollbarSize = std::max(state.scrollbar_size * scale,
95 font_size * 1.1f * scale);
96 style.GrabMinSize =
97 std::max(state.grab_min_size * scale, font_size * 0.9f * scale);
98 style.FramePadding.x = std::max(state.frame_padding.x * scale,
99 font_size * 0.55f * scale);
100 style.FramePadding.y = std::max(state.frame_padding.y * scale,
101 font_size * 0.35f * scale);
102 style.ItemSpacing.x = std::max(state.item_spacing.x * scale,
103 font_size * 0.45f * scale);
104 style.ItemSpacing.y = std::max(state.item_spacing.y * scale,
105 font_size * 0.35f * scale);
106 }
107
108 // Window chrome sizing for touch-friendly interaction
109 style.WindowRounding = 8.0f * scale;
110 style.PopupRounding = 6.0f * scale;
111 style.TabRounding = 4.0f * scale;
112 style.ScrollbarRounding = 6.0f * scale;
113 style.TabCloseButtonMinWidthUnselected = 44.0f * scale;
114 style.WindowMinSize = ImVec2(200.0f * scale, 150.0f * scale);
115 }
116
117 // Update safe area padding only when values actually change, to prevent
118 // frame-by-frame layout oscillation that causes dashboard flickering.
119 const UIEdgeInsets insets = GetSafeAreaInsets(view);
120 const float safe_x = std::max((float)insets.left, (float)insets.right);
121 const float safe_y = std::max((float)insets.top, (float)insets.bottom);
122
123 // Truncate to integer pixels to avoid sub-pixel oscillation.
124 const float stable_x = std::floor(safe_x);
125 const float stable_y = std::floor(safe_y);
126
127 if (stable_x != state.last_safe_x || stable_y != state.last_safe_y) {
128 state.last_safe_x = stable_x;
129 state.last_safe_y = stable_y;
130 style.DisplaySafeAreaPadding = ImVec2(stable_x, stable_y);
131 }
132
133 ios::SetSafeAreaInsets(insets.left, insets.right, insets.top,
134 insets.bottom);
135}
136#endif
137} // namespace
138
139absl::Status IOSWindowBackend::Initialize(const WindowConfig& config) {
140#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
142 if (!metal_view_) {
143 return absl::FailedPreconditionError("Metal view not set");
144 }
145
146 title_ = config.title;
147 status_.is_active = true;
148 status_.is_focused = true;
150
151 auto* view = static_cast<MTKView*>(metal_view_);
152 status_.width = static_cast<int>(view.bounds.size.width);
153 status_.height = static_cast<int>(view.bounds.size.height);
154
155 initialized_ = true;
156 return absl::OkStatus();
157#else
158 (void)config;
159 return absl::FailedPreconditionError(
160 "IOSWindowBackend is only available on iOS");
161#endif
162}
163
166
167#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
168 if (command_queue_) {
169 CFRelease(command_queue_);
170 command_queue_ = nullptr;
171 }
172#endif
173
174 metal_view_ = nullptr;
175 initialized_ = false;
176 return absl::OkStatus();
177}
178
180 return initialized_;
181}
182
184 out_event = WindowEvent{};
185 return false;
186}
187
188void IOSWindowBackend::ProcessNativeEvent(void* native_event) {
189 (void)native_event;
190}
191
193 WindowStatus status = status_;
194#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
195 if (metal_view_) {
196 auto* view = static_cast<MTKView*>(metal_view_);
197 status.width = static_cast<int>(view.bounds.size.width);
198 status.height = static_cast<int>(view.bounds.size.height);
199 }
200#endif
201 return status;
202}
203
205 return status_.is_active;
206}
207
209 status_.is_active = active;
210}
211
212void IOSWindowBackend::GetSize(int* width, int* height) const {
213#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
214 if (metal_view_) {
215 auto* view = static_cast<MTKView*>(metal_view_);
216 if (width) {
217 *width = static_cast<int>(view.bounds.size.width);
218 }
219 if (height) {
220 *height = static_cast<int>(view.bounds.size.height);
221 }
222 return;
223 }
224#endif
225
226 if (width) {
227 *width = 0;
228 }
229 if (height) {
230 *height = 0;
231 }
232}
233
234void IOSWindowBackend::SetSize(int width, int height) {
235#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
236 if (metal_view_) {
237 auto* view = static_cast<MTKView*>(metal_view_);
238 view.drawableSize = CGSizeMake(width, height);
239 }
240#else
241 (void)width;
242 (void)height;
243#endif
244}
245
246std::string IOSWindowBackend::GetTitle() const {
247 return title_;
248}
249
250void IOSWindowBackend::SetTitle(const std::string& title) {
251 title_ = title;
252}
253
255#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
256 if (metal_view_) {
257 auto* view = static_cast<MTKView*>(metal_view_);
258 view.hidden = NO;
259 }
260#endif
261 status_.is_active = true;
262}
263
265#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
266 if (metal_view_) {
267 auto* view = static_cast<MTKView*>(metal_view_);
268 view.hidden = YES;
269 }
270#endif
271 status_.is_active = false;
272}
273
275 if (!renderer || !metal_view_) {
276 return false;
277 }
278
279 if (renderer->GetBackendRenderer()) {
280 return true;
281 }
282
283 auto* metal_renderer = dynamic_cast<gfx::MetalRenderer*>(renderer);
284 if (metal_renderer) {
285 metal_renderer->SetMetalView(metal_view_);
286 } else {
287 LOG_WARN("IOSWindowBackend", "Non-Metal renderer selected on iOS");
288 }
289
290 return renderer->Initialize(nullptr);
291}
292
294 return nullptr;
295}
296
298 if (imgui_initialized_) {
299 return absl::OkStatus();
300 }
301
302 if (!renderer) {
303 return absl::InvalidArgumentError("Renderer is null");
304 }
305
306#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
307 if (!metal_view_) {
308 return absl::FailedPreconditionError("Metal view not set");
309 }
310
311 auto* view = static_cast<MTKView*>(metal_view_);
312 id<MTLDevice> device = view.device;
313 if (!device) {
314 device = MTLCreateSystemDefaultDevice();
315 view.device = device;
316 }
317
318 if (!device) {
319 return absl::InternalError("Failed to create Metal device");
320 }
321
322 IMGUI_CHECKVERSION();
323 ImGui::CreateContext();
324
325 ImGuiIO& io = ImGui::GetIO();
327 io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
328 io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
329 io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
330
331 if (auto ini_path = util::PlatformPaths::GetImGuiIniPath(); ini_path.ok()) {
332 static std::string ini_path_str;
333 if (ini_path_str.empty()) {
334 ini_path_str = ini_path->string();
335 }
336 io.IniFilename = ini_path_str.c_str();
337 } else {
338 io.IniFilename = nullptr;
339 LOG_WARN("IOSWindowBackend", "Failed to resolve ImGui ini path: %s",
340 ini_path.status().ToString().c_str());
341 }
342
343 if (!ImGui_ImplMetal_Init(device)) {
344 return absl::InternalError("ImGui_ImplMetal_Init failed");
345 }
346
347 auto font_status = LoadPackageFonts();
348 if (!font_status.ok()) {
349 ImGui_ImplMetal_Shutdown();
350 ImGui::DestroyContext();
351 return font_status;
352 }
354#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
355 ApplyTouchStyle(view);
356#endif
357
358 if (!command_queue_) {
359 id<MTLCommandQueue> queue = [device newCommandQueue];
360 command_queue_ = (__bridge_retained void*)queue;
361 }
362
363 imgui_initialized_ = true;
364 LOG_INFO("IOSWindowBackend", "ImGui initialized with Metal backend");
365 return absl::OkStatus();
366#else
367 return absl::FailedPreconditionError(
368 "IOSWindowBackend is only available on iOS");
369#endif
370}
371
373 if (!imgui_initialized_) {
374 return;
375 }
376
377 ImGui_ImplMetal_Shutdown();
378 ImGui::DestroyContext();
379
380 imgui_initialized_ = false;
381}
382
384 if (!imgui_initialized_) {
385 return;
386 }
387
388#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
389 auto* view = static_cast<MTKView*>(metal_view_);
390 if (!view) {
391 return;
392 }
393
394 ApplyTouchStyle(view);
395
396 auto* render_pass = view.currentRenderPassDescriptor;
397 if (!render_pass) {
398 return;
399 }
400
401 ImGui_ImplMetal_NewFrame(render_pass);
402#endif
403}
404
406 if (!imgui_initialized_) {
407 return;
408 }
409
410 // Clamp all floating windows within safe area to prevent off-screen drift.
411#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
412 {
413 ImGuiContext* ctx = ImGui::GetCurrentContext();
414 const ImGuiViewport* vp = ImGui::GetMainViewport();
415 if (ctx && vp) {
416 const auto insets = ios::GetSafeAreaInsets();
417 const float safe_left = std::max(0.0f, insets.left);
418 const float safe_right = std::max(0.0f, insets.right);
419 const float safe_bottom = std::max(0.0f, insets.bottom);
420 // Use overlay inset only for the top edge; bottom stays tied to home
421 // indicator safe area to avoid oscillating clamp bounds.
422 const float safe_top =
423 std::max(std::max(0.0f, insets.top), ios::GetOverlayTopInset());
424
425 const ImVec2 rect_pos(vp->WorkPos.x + safe_left, vp->WorkPos.y + safe_top);
426 const ImVec2 rect_size(
427 std::max(1.0f, vp->WorkSize.x - safe_left - safe_right),
428 std::max(1.0f, vp->WorkSize.y - safe_top - safe_bottom));
429
430 for (ImGuiWindow* win : ctx->Windows) {
431 if (!win || win->Hidden || win->IsFallbackWindow) continue;
432 if (win->DockIsActive || win->DockNodeAsHost) continue;
433 if (win->Flags & ImGuiWindowFlags_NoMove) continue;
434 if (win->Flags & ImGuiWindowFlags_ChildWindow) continue;
435
437 win->Pos, win->Size, rect_pos, rect_size, 48.0f);
438 if (result.clamped) {
439 ImGui::SetWindowPos(win, result.pos, ImGuiCond_Always);
440 }
441 }
442 }
443 }
444#endif
445
446 ImGui::Render();
447
448#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
449 (void)renderer;
450 auto* view = static_cast<MTKView*>(metal_view_);
451 if (!view || !view.currentDrawable) {
452 return;
453 }
454
455 auto* render_pass = view.currentRenderPassDescriptor;
456 if (!render_pass || !command_queue_) {
457 return;
458 }
459
460 id<MTLCommandQueue> queue =
461 (__bridge id<MTLCommandQueue>)command_queue_;
462 id<MTLCommandBuffer> command_buffer = [queue commandBuffer];
463 id<MTLRenderCommandEncoder> encoder =
464 [command_buffer renderCommandEncoderWithDescriptor:render_pass];
465
466 ImGui_ImplMetal_RenderDrawData(ImGui::GetDrawData(), command_buffer, encoder);
467 [encoder endEncoding];
468 [command_buffer presentDrawable:view.currentDrawable];
469 [command_buffer commit];
470#else
471 (void)renderer;
472#endif
473}
474
476 return 0;
477}
478
479std::shared_ptr<int16_t> IOSWindowBackend::GetAudioBuffer() const {
480 return nullptr;
481}
482
484 return "iOS-Metal";
485}
486
488 return 0;
489}
490
491} // namespace platform
492} // namespace yaze
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
virtual bool Initialize(SDL_Window *window)=0
Initializes the renderer with a given window.
virtual void * GetBackendRenderer()=0
Provides an escape hatch to get the underlying, concrete renderer object.
void SetMetalView(void *view)
static WindowClampResult ClampWindowToRect(const ImVec2 &pos, const ImVec2 &size, const ImVec2 &rect_pos, const ImVec2 &rect_size, float min_visible=32.0f)
bool IsInitialized() const override
Check if the backend is initialized.
void ProcessNativeEvent(void *native_event) override
Process a native SDL event (for ImGui integration)
std::shared_ptr< int16_t > GetAudioBuffer() const override
Get audio buffer (for legacy audio management)
absl::Status InitializeImGui(gfx::IRenderer *renderer) override
Initialize ImGui backends for this window/renderer combo.
std::string GetBackendName() const override
Get backend name for debugging/logging.
bool PollEvent(WindowEvent &out_event) override
Poll and process pending events.
absl::Status Initialize(const WindowConfig &config) override
Initialize the window backend with configuration.
bool IsActive() const override
Check if window is still active (not closed)
void RenderImGui(gfx::IRenderer *renderer) override
Render ImGui draw data (and viewports if enabled)
bool InitializeRenderer(gfx::IRenderer *renderer) override
Initialize renderer for this window.
void SetTitle(const std::string &title) override
Set window title.
uint32_t GetAudioDevice() const override
Get audio device ID (for legacy audio buffer management)
SDL_Window * GetNativeWindow() override
Get the underlying SDL_Window pointer for ImGui integration.
std::string GetTitle() const override
Get window title.
void HideWindow() override
Hide the window.
absl::Status Shutdown() override
Shutdown the window backend and release resources.
void SetActive(bool active) override
Set window active state.
void GetSize(int *width, int *height) const override
Get window dimensions.
void ShutdownImGui() override
Shutdown ImGui backends.
void SetSize(int width, int height) override
Set window dimensions.
void NewImGuiFrame() override
Start a new ImGui frame.
int GetSDLVersion() const override
Get SDL version being used.
void ShowWindow() override
Show the window.
WindowStatus GetStatus() const override
Get current window status.
static absl::StatusOr< std::filesystem::path > GetImGuiIniPath()
Get the ImGui ini path for YAZE.
#define LOG_WARN(category, format,...)
Definition log.h:138
#define LOG_INFO(category, format,...)
Definition log.h:136
void ColorsYaze()
Definition style.cc:33
void ConfigureImGuiDiagnostics(ImGuiIO &io, ImGuiDiagnosticsBuild build)
Set Dear ImGui's error-recovery and ID-conflict options.
SafeAreaInsets GetSafeAreaInsets()
void SetSafeAreaInsets(float left, float right, float top, float bottom)
absl::Status LoadPackageFonts()
Window configuration parameters.
Definition iwindow.h:24
Platform-agnostic window event data.
Definition iwindow.h:65
Window backend status information.
Definition iwindow.h:117