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