Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,10 @@ qrc_*

# Mac stuff
.DS_Store


build/
android/


CMakeLists.txt.user
8 changes: 8 additions & 0 deletions AndroidResources.qrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<RCC>
<qresource prefix="/android">
<file alias="catalogs">catalogs</file>
<file alias="tutorial">tutorial</file>
<file alias="examples">examples</file>
<file alias="qtlogo.svg">src/ui/images/qtlogo.svg</file>
</qresource>
</RCC>
84 changes: 73 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ cmake_minimum_required(VERSION 3.14.0)
# set(CMAKE_CXX_COMPILER g++-13.2)

project(XaoS)
# Fix simulator architecture on Apple Silicon Macs
if(APPLE AND CMAKE_SYSTEM_NAME STREQUAL "iOS")
if(CMAKE_OSX_SYSROOT MATCHES "iphonesimulator")
# On Apple Silicon, CMake may default to arm64 for the simulator.
# However, standard Qt installations often only provide x86_64 for the simulator.
# This default targets both to support universal or custom Qt builds (like yours).
# You can override this in Qt Creator's CMake settings if needed.
if(NOT CMAKE_OSX_ARCHITECTURES)
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Build architectures for iOS Simulator")
endif()
endif()
endif()

option(DEEPZOOM "Use 128-bit float for deep zoom" OFF)
option(OPENGL "Use OpenGL" OFF)
Expand All @@ -114,9 +126,13 @@ add_definitions(-DUSE_SFFE -DSFFE_CMPLX_GSL)
set(CMAKE_FIND_PACKAGE_RESOLVE_SYMLINKS TRUE)

# by default, build a release (a non-debug executable)
if(NOT CMAKE_BUILD_TYPE)
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY VALUE Release)
endif(NOT CMAKE_BUILD_TYPE)
# if(NOT CMAKE_BUILD_TYPE)
# set_property(CACHE CMAKE_BUILD_TYPE PROPERTY VALUE Release)
# endif(NOT CMAKE_BUILD_TYPE)
# by default, build a release (a non-debug executable)
if(NOT CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

# set the project directory
get_filename_component(PROJECT_DIR ${CMAKE_SOURCE_DIR} PATH)
Expand All @@ -137,6 +153,12 @@ if(OPENGL)
find_package(OpenGL REQUIRED)
endif(OPENGL)

# Qt Quick/QML for mobile UI overlay (Android and iOS)
if(ANDROID OR (APPLE AND CMAKE_SYSTEM_NAME STREQUAL "iOS"))
find_package(Qt6 COMPONENTS Quick Qml QuickWidgets QuickControls2 REQUIRED)
add_definitions(-DUSE_QML_OVERLAY)
endif()

# OS specific stuff
# on macOS the QT libraries are usually not installed into the system library folders
if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
Expand Down Expand Up @@ -165,12 +187,15 @@ set_source_files_properties(${XaoS_ICON} PROPERTIES MACOSX_PACKAGE_LOCATION "Res
if (EMSCRIPTEN)
else()
# Multilingual support: *.ts -> *.qm
find_package(Qt6LinguistTools)
file(GLOB TRANSLATION_FILES ${CMAKE_CURRENT_SOURCE_DIR}/i18n/*.ts)
# qt_add_translation
set_source_files_properties(${TRANSLATION_FILES}
PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/i18n")
qt6_add_translation(QM_FILES ${TRANSLATION_FILES})
#..
# find_package(Qt6LinguistTools)
# file(GLOB TRANSLATION_FILES ${CMAKE_CURRENT_SOURCE_DIR}/i18n/*.ts)
# # qt_add_translation
# set_source_files_properties(${TRANSLATION_FILES}
# PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/i18n")
# qt6_add_translation(QM_FILES ${TRANSLATION_FILES})
#..
set(QM_FILES "")
endif (EMSCRIPTEN)

# grab all sources for executable
Expand All @@ -189,15 +214,52 @@ if(EMSCRIPTEN)
target_link_libraries(xaos PUBLIC Qt6::Widgets)
target_link_options(xaos PUBLIC -sASYNCIFY -O3 -flto)
else()
add_executable(XaoS MACOSX_BUNDLE
qt_add_executable(XaoS
${CXX_FILES}
${C_FILES}
${QM_FILES}
${XaoS_ICON}
src/ui/XaoS.qrc
XaoS.qrc
)
target_link_libraries(XaoS Qt6::Widgets Qt6::PrintSupport)

if(APPLE)
set_target_properties(XaoS PROPERTIES MACOSX_BUNDLE TRUE)
endif()

target_link_libraries(XaoS PRIVATE Qt6::Widgets Qt6::PrintSupport)

# Link Qt Quick/QML for mobile builds
if(ANDROID OR (APPLE AND CMAKE_SYSTEM_NAME STREQUAL "iOS"))
target_link_libraries(XaoS PRIVATE Qt6::Quick Qt6::Qml Qt6::QuickWidgets Qt6::QuickControls2)
endif()

# if(ANDROID)
# # Bundle data files as resources for Android
# file(GLOB_RECURSE ANDROID_DATA_FILES
# "catalogs/*"
# "tutorial/*"
# "examples/*"
# )
# qt_add_resources(XaoS "android_data"
# PREFIX "/android"
# FILES ${ANDROID_DATA_FILES}
# )
# set_target_properties(XaoS PROPERTIES
# QT_ANDROID_PACKAGE_NAME "com.xaos.XaoS"
# QT_ANDROID_VERSION_NAME ${PROJECT_VERSION}
# QT_ANDROID_VERSION_CODE 1
# )
# endif()

if(APPLE AND CMAKE_SYSTEM_NAME STREQUAL "iOS")
set_target_properties(XaoS PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER "com.xaos.XaoS"
MACOSX_BUNDLE_BUNDLE_NAME "XaoS"
MACOSX_BUNDLE_BUNDLE_VERSION "4.2.1"
MACOSX_BUNDLE_SHORT_VERSION_STRING "4.2.1"
)
endif()
endif(EMSCRIPTEN)

if(DEEPZOOM)
Expand Down
7 changes: 7 additions & 0 deletions src/ui/XaoS.qrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<RCC>
<qresource prefix="/" >
<file>images/xaosbig.png</file>
<file>images/qtlogo.svg</file>
<file>fonts/MaterialSymbolsOutlined.ttf</file>
<file>qml/MobileOverlay.qml</file>
<file>qml/SettingsSheet.qml</file>
<file>qml/GlassButton.qml</file>
<file>qml/GlassPill.qml</file>
<file>qml/BottomNavBar.qml</file>
</qresource>
</RCC>
Binary file added src/ui/fonts/MaterialSymbolsOutlined.ttf
Binary file not shown.
212 changes: 212 additions & 0 deletions src/ui/fractalbridge.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
#include "fractalbridge.h"
#include "mainwindow.h"
#include "ui_helper.h"
#include "fractal.h"
#include "xmenu.h"

#include <cmath>
#include <QDebug>
#include <QGuiApplication>
#include <QScreen>
#include <QWindow>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QEvent>

FractalBridge::FractalBridge(MainWindow *window, QObject *parent)
: QObject(parent), m_window(window)
{
}

void FractalBridge::setUih(struct uih_context *uih)
{
m_uih = uih;
}

QString FractalBridge::zoomLevel() const { return m_zoomLevel; }
QString FractalBridge::realCoord() const { return m_realCoord; }
QString FractalBridge::imagCoord() const { return m_imagCoord; }
int FractalBridge::maxIterations() const { return m_maxIter; }
QString FractalBridge::fractalName() const { return m_fractalName; }
bool FractalBridge::autopilotActive() const { return m_autopilot; }

void FractalBridge::refreshState()
{
if (!m_uih || !m_uih->fcontext)
return;

bool changed = false;

// Calculate zoom level as a power-of-10 exponent
const fractal_context *fc = m_uih->fcontext;
double viewWidth = fc->s.ri - fc->s.rr; // approximate view span
if (viewWidth <= 0) viewWidth = 4.0;
int zoomExp = (int)std::max(0.0, -std::log10(std::abs(viewWidth) / 4.0));
QString newZoom = QString("DEEP ZOOM 10%1").arg(
zoomExp > 0 ? QString::fromUtf8("\u00B9\u2070").left(0) +
QString::number(zoomExp) : "⁰");
// Simpler: show as "10^N"
newZoom = QString("DEEP ZOOM 10^%1").arg(zoomExp);
if (newZoom != m_zoomLevel) { m_zoomLevel = newZoom; changed = true; }

// Real and imaginary coordinates of the center
QString newRe = QString("RE: %1").arg(
QString::number((double)fc->s.cr, 'f', 10));
QString newIm = QString("IM: %1").arg(
QString::number((double)fc->s.ci, 'f', 10));
if (newRe != m_realCoord) { m_realCoord = newRe; changed = true; }
if (newIm != m_imagCoord) { m_imagCoord = newIm; changed = true; }

// Max iterations
int newIter = (int)fc->maxiter;
if (newIter != m_maxIter) { m_maxIter = newIter; changed = true; }

// Fractal name
QString newName;
if (fc->currentformula)
newName = fc->currentformula->name[!fc->mandelbrot];
if (newName != m_fractalName) { m_fractalName = newName; changed = true; }

// Autopilot
bool newAP = m_uih->autopilot != 0;
if (newAP != m_autopilot) { m_autopilot = newAP; changed = true; }

if (changed)
emit stateChanged();
}

void FractalBridge::executeCommand(const QString &command)
{
if (!m_uih) return;

const menuitem *item = menu_findcommand(command.toUtf8().constData());
if (item) {
qDebug() << "FractalBridge::executeCommand:" << command;
// Use menuActivate through the MainWindow
m_window->menuActivateFromBridge(item, nullptr);
} else {
qDebug() << "FractalBridge: unknown command:" << command;
}
}

void FractalBridge::openMenu(const QString &menuName)
{
if (!m_uih) return;
qDebug() << "FractalBridge::openMenu:" << menuName;
m_window->popupMenuFromBridge(menuName.toUtf8().constData());
}

void FractalBridge::startZoomIn()
{
qDebug() << "FractalBridge::startZoomIn";
m_window->setSyntheticButtons(BUTTON1);
}

void FractalBridge::startZoomOut()
{
qDebug() << "FractalBridge::startZoomOut";
m_window->setSyntheticButtons(BUTTON3);
}

void FractalBridge::stopZoom()
{
qDebug() << "FractalBridge::stopZoom";
m_window->setSyntheticButtons(0);
}

void FractalBridge::gesturePinch(double scale, double centerX, double centerY)
{
// scale is cumulative since start of gesture. Compare with last scale.
double delta = scale / m_lastPinchScale;

// Use a small threshold (e.g. 2%) to avoid jitter
if (delta > 1.02 || delta < 0.98) {
int ticks = (delta > 1.0) ? 120 : -120;

QPointF pos(centerX, centerY);
QWheelEvent wheel(pos, pos, QPoint(0, 0), QPoint(0, ticks),
Qt::NoButton, Qt::NoModifier, Qt::ScrollUpdate, false);
QCoreApplication::sendEvent(m_window->centralWidget(), &wheel);

m_lastPinchScale = scale;
}
}

void FractalBridge::gesturePinchStarted()
{
m_lastPinchScale = 1.0;
}

void FractalBridge::gesturePan(double dx, double dy, double centerX, double centerY)
{
// Simulate mouse move for panning
// XaoS expects mouse BUTTON2 (middle) for panning
QPointF pos(centerX, centerY);

// Ensure BUTTON2 is set during move
m_window->setSyntheticButtons(BUTTON2);

QMouseEvent move(QEvent::MouseMove, pos, pos, pos,
Qt::MiddleButton, Qt::MiddleButton, Qt::NoModifier);
QCoreApplication::sendEvent(m_window->centralWidget(), &move);
}

void FractalBridge::gesturePanFinished()
{
qDebug() << "FractalBridge::gesturePanFinished";
m_window->setSyntheticButtons(0);
}

int FractalBridge::safeTop() const { return m_safeTop; }
int FractalBridge::safeBottom() const { return m_safeBottom; }
int FractalBridge::safeLeft() const { return m_safeLeft; }
int FractalBridge::safeRight() const { return m_safeRight; }

void FractalBridge::updateSafeAreaInsets()
{
int top = 0, bottom = 0, left = 0, right = 0;

QWindow *win = m_window ? m_window->windowHandle() : nullptr;

#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
// Qt 6.8+ provides safeAreaMargins() directly
if (win) {
QMarginsF margins = win->safeAreaMargins();
top = qRound(margins.top());
bottom = qRound(margins.bottom());
left = qRound(margins.left());
right = qRound(margins.right());
}
#else
// Fallback: use platform-specific defaults
Q_UNUSED(win);
#if defined(Q_OS_IOS)
// iPhone with notch / Dynamic Island
top = 59;
bottom = 34;
#elif defined(Q_OS_ANDROID)
// Android status bar
top = 24;
#endif
#endif

// Apply minimum safe margins on mobile even if platform reports 0
#if defined(Q_OS_IOS)
if (top < 20) top = 20;
if (bottom < 20) bottom = 20;
#elif defined(Q_OS_ANDROID)
if (top < 24) top = 24;
#endif

bool changed = (top != m_safeTop || bottom != m_safeBottom ||
left != m_safeLeft || right != m_safeRight);
m_safeTop = top;
m_safeBottom = bottom;
m_safeLeft = left;
m_safeRight = right;

if (changed) {
qDebug() << "Safe area insets:" << top << bottom << left << right;
emit safeAreaChanged();
}
}
Loading