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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
Nexoniarz 2026-10-19 23:07:37 +02:00
commit a5e0d2a9eb
5 changed files with 87 additions and 16 deletions

View file

@ -19,7 +19,7 @@
android:name="io.github.nexoniarz.fry3dx.FryActivity" android:name="io.github.nexoniarz.fry3dx.FryActivity"
android:exported="true" android:exported="true"
android:label="Fry3DX" android:label="Fry3DX"
android:screenOrientation="sensorLandscape" android:screenOrientation="fullUser"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"> android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation">

View file

@ -1,5 +1,9 @@
package io.github.nexoniarz.fry3dx; package io.github.nexoniarz.fry3dx;
import android.os.Build;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import org.libsdl.app.SDLActivity; import org.libsdl.app.SDLActivity;
// Fry3DX's activity: SDL's, loading SDL and the app (libmain.so, whose SDL_main it runs). // 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() { protected String[] getLibraries() {
return new String[] {"SDL3", "main"}; 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);
}
} }

View file

@ -40,7 +40,11 @@ bool Window::init(const char* title, int width, int height, bool opengl) {
log::error("SDL_Init failed: {}", SDL_GetError()); log::error("SDL_Init failed: {}", SDL_GetError());
return false; 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_) { if (!window_) {
log::error("SDL_CreateWindow failed: {}", SDL_GetError()); log::error("SDL_CreateWindow failed: {}", SDL_GetError());
return false; return false;

View file

@ -227,6 +227,9 @@ bool Device::createSwapchain() {
vkDeviceWaitIdle(device_); vkDeviceWaitIdle(device_);
VkSwapchainKHR old = swapchain_; VkSwapchainKHR old = swapchain_;
auto sc = vkb::SwapchainBuilder(physical_, device_, surface_) 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_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) .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) .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.pSwapchains = &swapchain_;
pi.pImageIndices = &imageIndex_; pi.pImageIndices = &imageIndex_;
VkResult r = vkQueuePresentKHR(queue_, &pi); 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; if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) swapchainDirty_ = true;
else VK_CHECK(r); else VK_CHECK(r);

View file

@ -514,6 +514,7 @@ public:
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
if (touch()) touchFrame(io); if (touch()) touchFrame(io);
const ImGuiID dock = ImGui::DockSpaceOverViewport(0, nullptr, ImGuiDockNodeFlags_PassthruCentralNode); 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 (!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); 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); updateCamera(io);
@ -612,7 +613,7 @@ private:
Scene before; Scene before;
std::set<std::pair<int, int>> bevelEdges; std::set<std::pair<int, int>> bevelEdges;
} drag_; } drag_;
bool boxing_ = false, painting_ = false, lassoing_ = false; bool boxing_ = false, painting_ = false, lassoing_ = false, tapDraw_ = false;
ImVec2 boxStart_{0, 0}; ImVec2 boxStart_{0, 0};
std::vector<ImVec2> lasso_; std::vector<ImVec2> lasso_;
std::pair<int, int> cutHover_{-1, -1}; std::pair<int, int> cutHover_{-1, -1};
@ -664,7 +665,7 @@ private:
bool pasteOnSurface_ = true; bool pasteOnSurface_ = true;
uint32_t renamingFolder_ = 0; uint32_t renamingFolder_ = 0;
// Touch (set when it starts: fry_touch). // 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 { struct Gesture {
bool two = false, longDone = false, rightUp = false; bool two = false, longDone = false, rightUp = false;
ImVec2 mid{}, downPos{}; 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); 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 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; gest_.two = true, gest_.mid = mid, gest_.spread = spread;
boxing_ = lassoing_ = touchOrbit_ = false; boxing_ = lassoing_ = touchOrbit_ = tapDraw_ = false;
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
} }
if (gest_.two) { 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 (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) { if (!gest_.longDone && now() - gest_.downAt > 0.55 && !drag_.active) {
gest_.longDone = true; gest_.longDone = true;
boxing_ = lassoing_ = touchOrbit_ = false; boxing_ = lassoing_ = touchOrbit_ = tapDraw_ = false;
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false); io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Right, true); io.AddMouseButtonEvent(ImGuiMouseButton_Right, true);
gest_.rightUp = true; gest_.rightUp = true;
@ -4133,7 +4134,8 @@ private:
if (touchOrbit_ && moved && ImGui::IsMouseDown(ImGuiMouseButton_Left)) orbitBy(io.MouseDelta); if (touchOrbit_ && moved && ImGui::IsMouseDown(ImGuiMouseButton_Left)) orbitBy(io.MouseDelta);
if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) { if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
boxing_ = false; 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) { 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)); 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); 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 (!mouseInView(io)) return;
if (tool_ == Tool::Draw) { if (tool_ == Tool::Draw) {
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) if (!ImGui::IsMouseClicked(ImGuiMouseButton_Left)) return;
if (auto p = drawPoint(io)) { 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)
ImVec2 s0, s1; addDrawPoint(io);
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);
}
return; return;
} }
if (tool_ == Tool::Knife) { 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) 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. // Painting: on the model under the mouse.
void paintViewport(ImGuiIO& io) { void paintViewport(ImGuiIO& io) {
if (paint_.waiting) { 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 (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->AddRectFilled(boxStart_, io.MousePos, IM_COL32(255, 140, 60, 30));
dl->AddRect(boxStart_, io.MousePos, kAccent); 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); dl->AddCircleFilled(s, i == 0 ? 6.0f : 4.0f, i == 0 ? IM_COL32(255, 255, 255, 255) : kAccent);
prev = s; 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 (auto p = drawPoint(io); p && project(*p, s)) {
if (!drawPts_.empty()) dl->AddLine(prev, s, IM_COL32(255, 160, 90, 180), 1.5f); 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); dl->AddCircle(s, 5.0f, IM_COL32(255, 255, 255, 220), 0, 1.5f);
@ -4903,9 +4909,19 @@ private:
void defaultLayout(ImGuiID dock) { void defaultLayout(ImGuiID dock) {
layoutDone_ = true; layoutDone_ = true;
ImGuiDockNode* node = ImGui::DockBuilderGetNode(dock); 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::DockBuilderRemoveNodeChildNodes(dock);
ImGui::DockBuilderSetNodeSize(dock, ImGui::GetMainViewport()->WorkSize); // (splits are shares of this)
ImGuiID center = dock; 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, &center);
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) 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, &center); ImGuiID left = ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.2f, nullptr, &center);
const ImGuiID leftBottom = ImGui::DockBuilderSplitNode(left, ImGuiDir_Down, 0.45f, nullptr, &left); const ImGuiID leftBottom = ImGui::DockBuilderSplitNode(left, ImGuiDir_Down, 0.45f, nullptr, &left);
@ -5134,6 +5150,7 @@ private:
} }
void toolbox() { void toolbox() {
if (std::exchange(showToolbox_, false)) ImGui::SetNextWindowFocus();
ImGui::Begin("Toolbox", nullptr, ImGuiWindowFlags_NoCollapse); ImGui::Begin("Toolbox", nullptr, ImGuiWindowFlags_NoCollapse);
const float w = ImGui::GetContentRegionAvail().x; const float w = ImGui::GetContentRegionAvail().x;
if (tab_ == Tab::Paint) { if (tab_ == Tab::Paint) {