From a5e0d2a9eb27054bed60aa60f724e1c81e026a99 Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Mon, 19 Oct 2026 23:07:37 +0200 Subject: [PATCH] Phone: turns with the phone (portrait layout), fullscreen, picture the right way up; tap/drag fixes for touch Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP --- android/AndroidManifest.xml | 2 +- .../github/nexoniarz/fry3dx/FryActivity.java | 40 +++++++++++++++++ engine/platform/window.cpp | 6 ++- engine/rhi/vulkan/vk_device.cpp | 10 +++++ src/app.cpp | 45 +++++++++++++------ 5 files changed, 87 insertions(+), 16 deletions(-) diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 38ab730..856a836 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -19,7 +19,7 @@ android:name="io.github.nexoniarz.fry3dx.FryActivity" android:exported="true" android:label="Fry3DX" - android:screenOrientation="sensorLandscape" + android:screenOrientation="fullUser" android:launchMode="singleInstance" android:windowSoftInputMode="adjustResize" android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"> diff --git a/android/java/io/github/nexoniarz/fry3dx/FryActivity.java b/android/java/io/github/nexoniarz/fry3dx/FryActivity.java index f1b4fc4..2f86144 100644 --- a/android/java/io/github/nexoniarz/fry3dx/FryActivity.java +++ b/android/java/io/github/nexoniarz/fry3dx/FryActivity.java @@ -1,5 +1,9 @@ package io.github.nexoniarz.fry3dx; +import android.os.Build; +import android.view.WindowInsets; +import android.view.WindowInsetsController; + import org.libsdl.app.SDLActivity; // Fry3DX's activity: SDL's, loading SDL and the app (libmain.so, whose SDL_main it runs). @@ -8,4 +12,40 @@ public class FryActivity extends SDLActivity { protected String[] getLibraries() { return new String[] {"SDL3", "main"}; } + + // The whole screen: the status and navigation bars hidden (a swipe from the edge shows them + // for a moment). SDL's own way (the old system-UI flags) doesn't do it on new Androids. + @Override + protected void onResume() { + super.onResume(); + hideBars(); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (hasFocus) hideBars(); + } + + // SDL sets the bars again (its old way) whenever the window changes: hidden again after it. + @Override + protected boolean sendCommand(int command, Object data) { + final boolean sent = super.sendCommand(command, data); + if (command == COMMAND_CHANGE_WINDOW_STYLE) getWindow().getDecorView().post(new Runnable() { // (no lambdas: built against android.jar alone) + @Override + public void run() { + hideBars(); + } + }); + return sent; + } + + private void hideBars() { + if (Build.VERSION.SDK_INT < 30) return; + getWindow().setDecorFitsSystemWindows(false); + WindowInsetsController c = getWindow().getInsetsController(); + if (c == null) return; + c.hide(WindowInsets.Type.systemBars()); + c.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); + } } diff --git a/engine/platform/window.cpp b/engine/platform/window.cpp index 00ca734..359cfa0 100644 --- a/engine/platform/window.cpp +++ b/engine/platform/window.cpp @@ -40,7 +40,11 @@ bool Window::init(const char* title, int width, int height, bool opengl) { log::error("SDL_Init failed: {}", SDL_GetError()); return false; } - window_ = SDL_CreateWindow(title, width, height, (opengl ? SDL_WINDOW_OPENGL : SDL_WINDOW_VULKAN) | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY); + SDL_WindowFlags flags = (opengl ? SDL_WINDOW_OPENGL : SDL_WINDOW_VULKAN) | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY; +#if defined(__ANDROID__) + flags |= SDL_WINDOW_FULLSCREEN; // (the whole screen: no status or navigation bar over the edges) +#endif + window_ = SDL_CreateWindow(title, width, height, flags); if (!window_) { log::error("SDL_CreateWindow failed: {}", SDL_GetError()); return false; diff --git a/engine/rhi/vulkan/vk_device.cpp b/engine/rhi/vulkan/vk_device.cpp index 90628f7..4c29994 100644 --- a/engine/rhi/vulkan/vk_device.cpp +++ b/engine/rhi/vulkan/vk_device.cpp @@ -227,6 +227,9 @@ bool Device::createSwapchain() { vkDeviceWaitIdle(device_); VkSwapchainKHR old = swapchain_; auto sc = vkb::SwapchainBuilder(physical_, device_, surface_) +#if defined(__ANDROID__) // (the compositor turns the picture with the phone: drawn as it's seen) + .set_pre_transform_flags(VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) +#endif .set_desired_format({VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR}) .set_desired_present_mode(vsync_ ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_MAILBOX_KHR) .add_fallback_present_mode(vsync_ ? VK_PRESENT_MODE_FIFO_KHR : VK_PRESENT_MODE_IMMEDIATE_KHR) @@ -312,6 +315,13 @@ void Device::endFrame() { pi.pSwapchains = &swapchain_; pi.pImageIndices = &imageIndex_; VkResult r = vkQueuePresentKHR(queue_, &pi); +#if defined(__ANDROID__) // (suboptimal every frame while the phone's turned: the compositor turns it; only a new size matters) + if (r == VK_SUBOPTIMAL_KHR) { + uint32_t w = 0, h = 0; + window_->pixelSize(w, h); + if (w == extent_.width && h == extent_.height) r = VK_SUCCESS; + } +#endif if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) swapchainDirty_ = true; else VK_CHECK(r); diff --git a/src/app.cpp b/src/app.cpp index a10a156..2d4b924 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -514,6 +514,7 @@ public: ImGuiIO& io = ImGui::GetIO(); if (touch()) touchFrame(io); const ImGuiID dock = ImGui::DockSpaceOverViewport(0, nullptr, ImGuiDockNodeFlags_PassthruCentralNode); + if (touch() && (io.DisplaySize.y > io.DisplaySize.x) != portrait_) portrait_ = !portrait_, layoutDone_ = false; // (the phone turned: its layout) if (!layoutDone_) defaultLayout(dock); if (ImGuiDockNode* c = ImGui::DockBuilderGetCentralNode(dock)) viewMin_ = c->Pos, viewMax_ = ImVec2(c->Pos.x + c->Size.x, c->Pos.y + c->Size.y); updateCamera(io); @@ -612,7 +613,7 @@ private: Scene before; std::set> bevelEdges; } drag_; - bool boxing_ = false, painting_ = false, lassoing_ = false; + bool boxing_ = false, painting_ = false, lassoing_ = false, tapDraw_ = false; ImVec2 boxStart_{0, 0}; std::vector lasso_; std::pair cutHover_{-1, -1}; @@ -664,7 +665,7 @@ private: bool pasteOnSurface_ = true; uint32_t renamingFolder_ = 0; // Touch (set when it starts: fry_touch). - bool touchMode_ = false, panelsOpen_ = false, viewsOpen_ = false, shiftLatch_ = false, ctrlLatch_ = false, touchOrbit_ = false; + bool touchMode_ = false, portrait_ = false, showToolbox_ = false, panelsOpen_ = false, viewsOpen_ = false, shiftLatch_ = false, ctrlLatch_ = false, touchOrbit_ = false; struct Gesture { bool two = false, longDone = false, rightUp = false; ImVec2 mid{}, downPos{}; @@ -3260,7 +3261,7 @@ private: const float spread = std::max(std::hypot(fingers[0].x - fingers[1].x, fingers[0].y - fingers[1].y), 1.0f); if (!gest_.two && !drag_.active && inView(mid) && !ImGui::IsAnyItemActive()) { // two fingers: the first's tap or drag is off gest_.two = true, gest_.mid = mid, gest_.spread = spread; - boxing_ = lassoing_ = touchOrbit_ = false; + boxing_ = lassoing_ = touchOrbit_ = tapDraw_ = false; io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); } if (gest_.two) { @@ -3279,7 +3280,7 @@ private: if (std::hypot(p.x - gest_.downPos.x, p.y - gest_.downPos.y) > 12) gest_.downAt = now() + 1e9; // (moved: not a long press) if (!gest_.longDone && now() - gest_.downAt > 0.55 && !drag_.active) { gest_.longDone = true; - boxing_ = lassoing_ = touchOrbit_ = false; + boxing_ = lassoing_ = touchOrbit_ = tapDraw_ = false; io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); io.AddMouseButtonEvent(ImGuiMouseButton_Right, true); gest_.rightUp = true; @@ -4133,7 +4134,8 @@ private: if (touchOrbit_ && moved && ImGui::IsMouseDown(ImGuiMouseButton_Left)) orbitBy(io.MouseDelta); if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { boxing_ = false; - if (std::exchange(touchOrbit_, false) && moved) return; + if (std::exchange(touchOrbit_, false) && moved) return void(tapDraw_ = false); + if (std::exchange(tapDraw_, false)) return addDrawPoint(io); if (std::hypot(io.MousePos.x - boxStart_.x, io.MousePos.y - boxStart_.y) > 4) { const ImVec2 a(std::min(boxStart_.x, io.MousePos.x), std::min(boxStart_.y, io.MousePos.y)), b(std::max(boxStart_.x, io.MousePos.x), std::max(boxStart_.y, io.MousePos.y)); regionSelect([&](ImVec2 s) { return s.x >= a.x && s.x <= b.x && s.y >= a.y && s.y <= b.y; }, io.KeyShift, io.KeyCtrl, !io.KeyShift && !io.KeyCtrl); @@ -4159,12 +4161,9 @@ private: } if (!mouseInView(io)) return; if (tool_ == Tool::Draw) { - if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) - if (auto p = drawPoint(io)) { - ImVec2 s0, s1; - if (drawPts_.size() >= 3 && project(drawPts_[0], s0) && project(*p, s1) && std::hypot(s0.x - s1.x, s0.y - s1.y) < 10) finishShape(); - else drawPts_.push_back(*p); - } + if (!ImGui::IsMouseClicked(ImGuiMouseButton_Left)) return; + if (touch()) return void((boxing_ = touchOrbit_ = tapDraw_ = true, boxStart_ = io.MousePos)); // (a finger: a tap puts a point down when it lifts, a drag turns the view) + addDrawPoint(io); return; } if (tool_ == Tool::Knife) { @@ -4217,6 +4216,13 @@ private: else boxing_ = true, boxStart_ = io.MousePos, touchOrbit_ = touch() && tool_ != Tool::Select; // (a finger: dragging turns the view, but Select's box) } } + void addDrawPoint(const ImGuiIO& io) { + if (auto p = drawPoint(io)) { + ImVec2 s0, s1; + if (drawPts_.size() >= 3 && project(drawPts_[0], s0) && project(*p, s1) && std::hypot(s0.x - s1.x, s0.y - s1.y) < (touch() ? 24 : 10)) finishShape(); + else drawPts_.push_back(*p); + } + } // Painting: on the model under the mouse. void paintViewport(ImGuiIO& io) { if (paint_.waiting) { @@ -4748,7 +4754,7 @@ private: } if (pts.size() >= 3) dl->AddPolyline(pts.data(), int(pts.size()), hole_.active ? kHot : IM_COL32(255, 255, 255, 200), ImDrawFlags_Closed, 2.0f); } - if (boxing_ && std::hypot(io.MousePos.x - boxStart_.x, io.MousePos.y - boxStart_.y) > 4) { + if (boxing_ && !touchOrbit_ && std::hypot(io.MousePos.x - boxStart_.x, io.MousePos.y - boxStart_.y) > 4) { dl->AddRectFilled(boxStart_, io.MousePos, IM_COL32(255, 140, 60, 30)); dl->AddRect(boxStart_, io.MousePos, kAccent); } @@ -4763,7 +4769,7 @@ private: dl->AddCircleFilled(s, i == 0 ? 6.0f : 4.0f, i == 0 ? IM_COL32(255, 255, 255, 255) : kAccent); prev = s; } - if (mouseInView(io)) + if (mouseInView(io) && !touch()) // (a finger isn't hovering anywhere) if (auto p = drawPoint(io); p && project(*p, s)) { if (!drawPts_.empty()) dl->AddLine(prev, s, IM_COL32(255, 160, 90, 180), 1.5f); dl->AddCircle(s, 5.0f, IM_COL32(255, 255, 255, 220), 0, 1.5f); @@ -4903,9 +4909,19 @@ private: void defaultLayout(ImGuiID dock) { layoutDone_ = true; ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock); - if (node && (node->IsSplitNode() || node->Windows.Size)) return; // (the user's own, from the .ini) + if (!touch() && node && (node->IsSplitNode() || node->Windows.Size)) return; // (the user's own, from the .ini) ImGui::DockBuilderRemoveNodeChildNodes(dock); + ImGui::DockBuilderSetNodeSize(dock, ImGui::GetMainViewport()->WorkSize); // (splits are shares of this) ImGuiID center = dock; + if (touch() && portrait_) { // a phone held upright: the view on top, the panels as tabs under it + const ImGuiID bottom = ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, 0.36f, nullptr, ¢er); + const ImVec2 size = ImGui::GetMainViewport()->WorkSize; + ImGui::DockBuilderSetNodeSize(bottom, ImVec2(size.x, size.y * 0.36f)); + for (const char* w : {"Toolbox", "Tool options", "Properties", "Layers", "Picture"}) ImGui::DockBuilderDockWindow(w, bottom); + ImGui::DockBuilderFinish(dock); + showToolbox_ = true; // (its tab first) + return; + } if (touch()) { // a phone: tools down the left, the panels on the right (shown when asked for) ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, ¢er); const ImGuiID leftBottom = ImGui::DockBuilderSplitNode(left, ImGuiDir_Down, 0.45f, nullptr, &left); @@ -5134,6 +5150,7 @@ private: } void toolbox() { + if (std::exchange(showToolbox_, false)) ImGui::SetNextWindowFocus(); ImGui::Begin("Toolbox", nullptr, ImGuiWindowFlags_NoCollapse); const float w = ImGui::GetContentRegionAvail().x; if (tab_ == Tab::Paint) {