Improved android networking

This commit is contained in:
Yousef Khadadeh 2026-05-14 11:53:36 +03:00
parent a533494b3a
commit ad1844adc7
27 changed files with 568 additions and 69 deletions

View file

@ -56,6 +56,7 @@ set(sources "src/main.cpp"
"src/GlobalConfig.cpp"
"src/RichTextUserInput.cpp"
"src/CustomEvents.cpp"
"src/NetThreadManager.cpp"
"src/Screens/Screen.cpp"
"src/Screens/DrawingProgramScreen.cpp"
"src/Screens/DesktopDrawingProgramScreen.cpp"

View file

@ -32,7 +32,7 @@ android {
ndkVersion = "29.0.14206865"
defaultConfig {
applicationId 'com.erroratline0.InfiniPaint'
minSdkVersion 21
minSdkVersion 26
targetSdk 35
versionCode 1
versionName "1.0"

View file

@ -20,6 +20,8 @@
void startTextInput(long, java.lang.String, int, int, int);
void updateCursorPos(int, int);
void updateTextboxAndCursorPos(java.lang.String, int, int);
void startNetworkService();
void stopNetworkService();
}
-keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity {

View file

@ -58,6 +58,9 @@
<!-- if you want to connect to the network or internet, uncomment this. -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<!-- Create a Java class extending SDLActivity and place it in a
directory under app/src/main/java matching the package, e.g. app/src/main/java/com/gamemaker/game/MyGame.java
@ -76,6 +79,13 @@
<!-- Example of setting SDL hints from AndroidManifest.xml:
<meta-data android:name="SDL_ENV.SDL_ANDROID_TRAP_BACK_BUTTON" android:value="0"/>
-->
<service
android:name=".InfiniPaintNetworkService"
android:foregroundServiceType="specialUse"
android:exported="false">
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="When connected to a server/hosting a server, keep the connection alive even when the application is in the background."/>
</service>
<activity android:name="com.erroratline0.infinipaint.InfiniPaint"
android:label="@string/app_name"

View file

@ -19,6 +19,7 @@
package com.erroratline0.infinipaint;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@ -142,4 +143,10 @@ public class InfiniPaint extends SDLActivity {
static public void updateTextboxAndCursorPos(String str, int selectionBegin, int selectionEnd) {
mSingleton.commandHandler.post(new InfiniPaintUpdateTextboxAndCursorPosTask(str, selectionBegin, selectionEnd));
}
static public void startNetworkService() {
mSingleton.startForegroundService(new Intent(mSingleton, InfiniPaintNetworkService.class));
}
static public void stopNetworkService() {
mSingleton.stopService(new Intent(mSingleton, InfiniPaintNetworkService.class));
}
}

View file

@ -0,0 +1,58 @@
package com.erroratline0.infinipaint;
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.IBinder;
import android.view.KeyEvent;
public class InfiniPaintNetworkService extends Service {
public static native void networkServiceStarted();
public static native void networkServiceDestroyed();
private static final String CHANNEL_ID = "NetworkServiceChannel";
@Override
public void onCreate() {
createNotificationChannel();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Notification notification = new Notification.Builder(this, CHANNEL_ID)
.setContentTitle("Online canvas")
.setContentText("Currently online")
.build();
startForeground(1, notification);
networkServiceStarted();
// For now, don't try restarting the connection after we've been disconnected
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
// We don't provide binding, so return null
return null;
}
@Override
public void onDestroy() {
networkServiceDestroyed();
}
private void createNotificationChannel() {
NotificationChannel serviceChannel = new NotificationChannel(
CHANNEL_ID,
"Network Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(serviceChannel);
}
}

View file

@ -78,10 +78,7 @@ NetLibrary::LoadP2PSettingsInPathResult NetLibrary::load_p2p_settings_in_path(co
return LoadP2PSettingsInPathResult::SUCCESS;
}
void NetLibrary::init(const std::filesystem::path& p2pConfigPath) {
if(alreadyInitialized)
return;
void NetLibrary::init_config(const std::filesystem::path& p2pConfigPath) {
rtc::InitLogger(rtc::LogLevel::Info, [](rtc::LogLevel level, std::string message) {
Logger::get().log("INFO", "[LibDataChannel Log Level " + std::to_string(static_cast<int>(level)) + "] " + message);
});
@ -104,7 +101,11 @@ void NetLibrary::init(const std::filesystem::path& p2pConfigPath) {
}
break;
}
}
void NetLibrary::init_websocket() {
if(alreadyInitialized)
return;
// Not sure why, but TLS verification fails on mac
// Should be fixed later, but for now this is fine, as the signaling server doesn't have
// critical information
@ -130,7 +131,7 @@ void NetLibrary::init(const std::filesystem::path& p2pConfigPath) {
ws->onClosed([]() {
Logger::get().log("INFO", "Websocket closed");
destroy();
destroy_websocket();
});
ws->onMessage([wws = make_weak_ptr(ws)](auto data) {
@ -187,7 +188,6 @@ void NetLibrary::init(const std::filesystem::path& p2pConfigPath) {
Logger::get().log("INFO", "NetLibrary waiting for signaling to be connected...");
//wsFuture.get();
alreadyInitialized = true;
}
@ -228,7 +228,7 @@ void NetLibrary::register_server(std::shared_ptr<NetServer> server) {
void NetLibrary::register_client(std::shared_ptr<NetClient> client) {
client->localID = Random::get().alphanumeric_str(LOCALID_LEN);
std::scoped_lock serverListLock(clientListMutex);
std::scoped_lock clientListLock(clientListMutex);
clients.emplace_back(client);
}
@ -429,7 +429,14 @@ MessageOrder NetLibrary::get_message_order(rtc::binary& message) {
return u.o;
}
void NetLibrary::destroy() {
bool NetLibrary::is_initialized() {
return alreadyInitialized;
}
void NetLibrary::destroy_websocket() {
if(!alreadyInitialized)
return;
ws = nullptr;
{
std::scoped_lock peerListLock(peerListMutex);
@ -454,6 +461,6 @@ void NetLibrary::destroy() {
servers.clear();
}
wsPromise = std::promise<void>();
//globalID.clear(); // Do we need to do this? (Causes problems when global ID changes from the user's side after they copy a lobby address
alreadyInitialized = false;
//globalID.clear(); // Do we need to do this? (Causes problems when global ID changes from the user's side after they copy a lobby address
}

View file

@ -39,7 +39,8 @@ typedef uint8_t NetTypeIDType;
class NetLibrary {
public:
static void init(const std::filesystem::path& p2pConfigPath);
static void init_config(const std::filesystem::path& p2pConfigPath);
static void init_websocket();
static void copy_default_p2p_config_to_path(const std::filesystem::path& newP2PConfigPath);
static void update();
static std::string get_random_server_local_id();
@ -48,7 +49,8 @@ class NetLibrary {
static void register_server(std::shared_ptr<NetServer> server);
static void register_client(std::shared_ptr<NetClient> client);
static void destroy();
static bool is_initialized();
static void destroy_websocket();
static constexpr size_t LOCALID_LEN = 10;
static constexpr size_t GLOBALID_LEN = 40;

View file

@ -25,6 +25,7 @@
#include <string>
#include <modules/skunicode/include/SkUnicode.h>
#include <modules/skunicode/include/SkUnicode_icu.h>
#include "NetThreadManager.hpp"
std::string jstring2string(JNIEnv *env, jstring jStr) {
if (!jStr)
@ -94,6 +95,26 @@ namespace AndroidJNICalls {
env->DeleteLocalRef(clazz);
}
void startNetworkService() {
JNIEnv* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
jobject activity = (jobject)SDL_GetAndroidActivity();
jclass clazz = env->GetObjectClass(activity);
jmethodID method_id = env->GetStaticMethodID(clazz, "startNetworkService", "()V");
env->CallStaticVoidMethod(clazz, method_id);
env->DeleteLocalRef(activity);
env->DeleteLocalRef(clazz);
}
void stopNetworkService() {
JNIEnv* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
jobject activity = (jobject)SDL_GetAndroidActivity();
jclass clazz = env->GetObjectClass(activity);
jmethodID method_id = env->GetStaticMethodID(clazz, "stopNetworkService", "()V");
env->CallStaticVoidMethod(clazz, method_id);
env->DeleteLocalRef(activity);
env->DeleteLocalRef(clazz);
}
void updateModMap(CustomEvents::InputTextBoxID tId, const std::shared_ptr<RichText::TextStyleModifier::ModifierMap>& newModMap) {
std::scoped_lock a{textboxMutex};
if(tId == textboxID && newModMap && modMap)
@ -309,4 +330,16 @@ Java_com_erroratline0_infinipaint_InfiniPaintTextBoxInputConnection_nativeSetSel
});
}
#endif
extern "C"
JNIEXPORT void JNICALL
Java_com_erroratline0_infinipaint_InfiniPaintNetworkService_networkServiceStarted(JNIEnv *env, jclass clazz) {
NetThreadManager::get().init_thread();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_erroratline0_infinipaint_InfiniPaintNetworkService_networkServiceDestroyed(JNIEnv *env, jclass clazz) {
NetThreadManager::get().destroy_thread();
}
#endif

View file

@ -27,6 +27,8 @@ namespace AndroidJNICalls {
extern InputManager* globalInputManager;
void startTextInput(CustomEvents::InputTextBoxID newTextboxID, const std::shared_ptr<RichText::TextBox>& newTextbox, const std::shared_ptr<RichText::TextBox::Cursor>& newCursor, const std::shared_ptr<RichText::TextStyleModifier::ModifierMap>& modMap, int inputType);
void startNetworkService();
void stopNetworkService();
void updateCursorPos(CustomEvents::InputTextBoxID tId, const std::shared_ptr<RichText::TextBox::Cursor>& newCursor);
void updateModMap(CustomEvents::InputTextBoxID tId, const std::shared_ptr<RichText::TextStyleModifier::ModifierMap>& newModMap);
void updateTextboxAndCursorPos(CustomEvents::InputTextBoxID tId, const std::shared_ptr<RichText::TextBox>& newTextbox, const std::shared_ptr<RichText::TextBox::Cursor>& newCursor);

View file

@ -23,7 +23,7 @@
#include "Helpers/NetworkingObjects/NetObjTemporaryPtr.decl.hpp"
#include <Helpers/NetworkingObjects/NetObjWeakPtr.hpp>
#include <Helpers/NetworkingObjects/NetObjGenericSerializedClass.hpp>
#include "../../Screens/PhoneDrawingProgramScreen.hpp"
#include "../../GUIStuff/ElementHelpers/TextLabelHelpers.hpp"
#include "../../GUIStuff/ElementHelpers/LayoutHelpers.hpp"
#include "../../GUIStuff/ElementHelpers/TextBoxHelpers.hpp"
@ -130,6 +130,16 @@ void GridModifyTool::input_mouse_motion_callback(const InputManager::MouseMotion
}
}
Vector4f* GridModifyTool::color_picker_color(Vector4f* oldColor) {
NetworkingObjects::NetObjTemporaryPtr<WorldGrid> gLock = grid.lock();
if(gLock) {
WorldGrid& g = *gLock;
if(oldColor == &g.color)
return oldColor;
}
return nullptr;
}
void GridModifyTool::gui_toolbox(Toolbar& t) {
using namespace GUIStuff;
using namespace ElementHelpers;
@ -209,6 +219,10 @@ void GridModifyTool::gui_phone_toolbox(PhoneDrawingProgramScreen& t) {
checkbox_boolean_field(gui, "Subdivide outwards", "Subdivide when zooming out", &g.removeDivisionsOutwards, [&] {
g.set_remove_divisions_outwards(g.removeDivisionsOutwards);
});
left_to_right_line_layout(gui, [&]() {
t.color_selector_button("Grid Color", &g.color);
text_label(gui, "Grid Color");
});
auto bounded = std::make_shared<bool>(g.bounds.has_value());
checkbox_boolean_field(gui, "Bounded", "Bounded", bounded.get(), [&, bounded] {
if(*bounded) {

View file

@ -28,6 +28,7 @@ class GridModifyTool : public DrawingProgramToolBase {
GridModifyTool(DrawingProgram& initDrawP);
void set_grid(const NetworkingObjects::NetObjWeakPtr<WorldGrid>& newGrid);
virtual DrawingProgramToolType get_type() override;
virtual Vector4f* color_picker_color(Vector4f* oldColor) override;
virtual void gui_phone_toolbox(PhoneDrawingProgramScreen& t) override;
virtual void gui_toolbox(Toolbar& t) override;
virtual void right_click_popup_gui(Toolbar& t, Vector2f popupPos) override;

View file

@ -30,6 +30,7 @@
#include <include/core/SkData.h>
#include <include/core/SkSurfaceProps.h>
#include "InputManager.hpp"
#include "NetThreadManager.hpp"
#include <cereal/types/vector.hpp>
#include <cereal/types/unordered_map.hpp>
#include <Eigen/Core>
@ -90,12 +91,20 @@ void MainProgram::update() {
input.update();
g.update();
screen->update();
for(auto& w : worlds)
w->update();
NetLibrary::update();
background_update();
post_callback();
}
void MainProgram::background_update() {
bool isAnyWorldConnected = false;
for(auto& w : worlds) {
w->update();
isAnyWorldConnected |= ((w->netServer && !w->netServer->is_disconnected()) || (w->netClient && !w->netClient->is_disconnected()));
}
if(!isAnyWorldConnected)
NetThreadManager::get().destroy();
}
bool MainProgram::app_close_requested() {
return screen->app_close_requested();
}
@ -108,7 +117,7 @@ void MainProgram::update_display_names() {
}
void MainProgram::init_net_library() {
NetLibrary::init(conf.configPath / "p2p.json");
NetThreadManager::get().init(this);
}
void MainProgram::save_config() {
@ -183,6 +192,7 @@ void MainProgram::load_config() {
conf.load_licenses();
NetLibrary::copy_default_p2p_config_to_path(conf.configPath / "p2p.json");
NetLibrary::init_config(conf.configPath / "p2p.json");
update_display_names();
set_vsync_value(conf.vsyncValue);
@ -284,8 +294,8 @@ void MainProgram::switch_to_tab(size_t wIndex) {
void MainProgram::post_callback() {
g.gui.run_post_callback_func();
run_new_screen_func();
close_set_to_close_tabs();
run_new_screen_func();
g.gui.layout_if_necessary();
}
@ -293,6 +303,13 @@ void MainProgram::input_app_about_to_go_to_background_callback() {
screen->input_app_about_to_go_to_background_callback();
save_config();
post_callback();
NetThreadManager::get().go_to_background();
}
void MainProgram::input_app_about_to_go_to_foreground_callback() {
NetThreadManager::get().go_to_foreground();
screen->input_app_about_to_go_to_foreground_callback();
post_callback();
}
void MainProgram::input_add_file_to_canvas_callback(const CustomEvents::AddFileToCanvasEvent& addFile) {
@ -488,15 +505,8 @@ void MainProgram::close_set_to_close_tabs() {
}
return false;
});
if(worlds.empty())
create_new_tab({
.isClient = false
});
else if(world)
worldIndex = std::find(worlds.begin(), worlds.end(), world) - worlds.begin();
else
switch_to_tab(0);
tabsToClose.clear();
screen->on_tab_close();
g.gui.set_to_layout();
}
}
@ -524,5 +534,5 @@ void MainProgram::early_destroy() {
MainProgram::~MainProgram() {
screen = nullptr; // Destroy screen first to let destructor run
NetLibrary::destroy();
NetThreadManager::get().destroy();
}

View file

@ -111,6 +111,7 @@ class MainProgram {
MainProgram();
void update();
void background_update();
void draw_world(SkCanvas* canvas, std::shared_ptr<World> worldToDraw, const DrawData& drawData);
void draw(SkCanvas* canvas);
sk_sp<SkSurface> create_native_surface(Vector2i resolution, bool isMSAA);
@ -156,6 +157,7 @@ class MainProgram {
void input_android_text_box_input_callback(const CustomEvents::AndroidTextBoxInputEvent& textboxInput);
void input_app_about_to_go_to_background_callback();
void input_app_about_to_go_to_foreground_callback();
void input_global_back_button_callback();
bool input_keybind_callback(const Vector2ui32& newKey);
void input_drop_file_callback(const InputManager::DropCallbackArgs& drop);

79
src/NetThreadManager.cpp Normal file
View file

@ -0,0 +1,79 @@
#include "NetThreadManager.hpp"
#include <Helpers/Networking/NetLibrary.hpp>
#include "AndroidJNICalls.hpp"
#include "MainProgram.hpp"
NetThreadManager NetThreadManager::global;
NetThreadManager& NetThreadManager::get() {
return global;
}
void NetThreadManager::init(MainProgram* m) {
if(started)
return;
started = true;
main = m;
#ifndef __ANDROID__
init_thread();
#else
AndroidJNICalls::startNetworkService();
#endif
}
void NetThreadManager::init_thread() {
if(t)
throw std::runtime_error("[NetThreadManager::init_thread] Thread can't exist when init_thread is called");
destroyThread = false;
t = std::make_unique<std::thread>([&]{ thread_update(); });
}
void NetThreadManager::thread_update() {
NetLibrary::init_websocket();
for(;;) {
using namespace std::chrono_literals;
std::this_thread::sleep_for(20ms);
if(destroyThread) {
NetLibrary::destroy_websocket();
break;
}
{
std::scoped_lock l{backgroundUpdateMutex};
if(backgroundUpdate)
main->background_update();
}
NetLibrary::update();
}
#ifdef __ANDROID__
AndroidJNICalls::stopNetworkService();
#endif
}
void NetThreadManager::destroy_thread() {
if(!t)
throw std::runtime_error("[NetThreadManager::init_thread] Thread can't be null when destroy_thread is called");
destroyThread = true;
t->join();
t = nullptr;
}
void NetThreadManager::go_to_background() {
std::scoped_lock l{backgroundUpdateMutex};
backgroundUpdate = true;
}
void NetThreadManager::go_to_foreground() {
std::scoped_lock l{backgroundUpdateMutex};
backgroundUpdate = false;
}
void NetThreadManager::destroy() {
if(!started)
return;
started = false;
#ifndef __ANDROID__
destroy_thread();
#else
AndroidJNICalls::stopNetworkService();
#endif
}

27
src/NetThreadManager.hpp Normal file
View file

@ -0,0 +1,27 @@
#pragma once
#include <thread>
#include <mutex>
#include <functional>
class MainProgram;
class NetThreadManager {
public:
static NetThreadManager& get();
void init(MainProgram* m);
void thread_update();
void destroy();
void init_thread();
void destroy_thread();
void go_to_background();
void go_to_foreground();
private:
static NetThreadManager global;
MainProgram* main;
std::mutex backgroundUpdateMutex;
bool backgroundUpdate = false;
std::unique_ptr<std::thread> t;
std::atomic<bool> destroyThread = false;
bool started = false;
};

View file

@ -83,3 +83,14 @@ void DesktopDrawingProgramScreen::open_file_selector(const std::string& filePick
else
toolbar.open_file_selector_non_native(filePickerName, extensionFilters, postSelectionFunc, fileName, isSaving);
}
void DesktopDrawingProgramScreen::on_tab_close() {
if(main.worlds.empty())
main.create_new_tab({
.isClient = false
});
else if(main.world)
main.worldIndex = std::find(main.worlds.begin(), main.worlds.end(), main.world) - main.worlds.begin();
else
main.switch_to_tab(0);
}

View file

@ -29,6 +29,7 @@ class DesktopDrawingProgramScreen : public DrawingProgramScreen {
virtual void input_key_callback(const InputManager::KeyCallbackArgs& key) override;
virtual void input_text_key_callback(const InputManager::KeyCallbackArgs& key) override;
virtual void open_file_selector(const std::string& filePickerName, const std::vector<ExtensionFilter>& extensionFilters, OpenFileSelectorCallback postSelectionFunc, const std::string& fileName = "", bool isSaving = false) override;
virtual void on_tab_close() override;
private:
Toolbar toolbar;

View file

@ -220,6 +220,9 @@ void FileSelectScreen::main_display() {
case SelectedMenu::TRASH:
file_view();
break;
case SelectedMenu::CONNECT:
connect_view();
break;
case SelectedMenu::SETTINGS:
settings_view();
break;
@ -491,13 +494,16 @@ void FileSelectScreen::title_bar() {
});
switch(selectedMenu) {
case SelectedMenu::FILES:
mutable_text_label(gui, "main screen header text", "Files");
mutable_text_label(gui, "main screen header text files", "Files");
break;
case SelectedMenu::TRASH:
mutable_text_label(gui, "main screen header text", "Trash");
mutable_text_label(gui, "main screen header text trash", "Trash");
break;
case SelectedMenu::CONNECT:
mutable_text_label(gui, "main screen header text connect", "Connect");
break;
case SelectedMenu::SETTINGS:
mutable_text_label(gui, "main screen header text", "Settings");
mutable_text_label(gui, "main screen header text settings", "Settings");
break;
}
CLAY_AUTO_ID({
@ -696,6 +702,11 @@ void FileSelectScreen::main_menu() {
mainMenuOpenAnim->animation_trigger_reverse();
if(mainViewScrollArea) mainViewScrollArea->reset_scroll();
});
icon_text_transparent_option_selected_button("Connect", "data/icons/network.svg", "Connect", selectedMenu == SelectedMenu::CONNECT, [&] {
selectedMenu = SelectedMenu::CONNECT;
mainMenuOpenAnim->animation_trigger_reverse();
if(mainViewScrollArea) mainViewScrollArea->reset_scroll();
});
icon_text_transparent_option_selected_button("Settings", "data/icons/RemixIcon/settings-3-line.svg", "Settings", selectedMenu == SelectedMenu::SETTINGS, [&] {
selectedMenu = SelectedMenu::SETTINGS;
mainMenuOpenAnim->animation_trigger_reverse();
@ -854,6 +865,50 @@ void FileSelectScreen::file_view() {
}
}
void FileSelectScreen::connect_view() {
auto& gui = main.g.gui;
mainViewScrollArea = gui.element<ScrollArea>("connect scroll area", ScrollArea::Options{
.scrollVertical = true,
.clipVertical = true,
.scrollbarY = ScrollArea::ScrollbarType::NORMAL,
.innerContent = [&] (auto&) {
CLAY_AUTO_ID({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)},
.layoutDirection = CLAY_LEFT_TO_RIGHT
},
}) {
CLAY_AUTO_ID({
.layout = {
.sizing = {.width = CLAY_SIZING_GROW(0, 300), .height = CLAY_SIZING_GROW(0)},
.childGap = gui.io.theme->childGap1,
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
}) {
input_text_field(gui, "Connect lobby field", "Lobby", &connectLobbyStr);
text_button(gui, "Connect button", "Connect", {
.wide = true,
.onClick = [&] {
CustomEvents::emit_event<CustomEvents::OpenInfiniPaintFileEvent>({
.isClient = true,
.netSource = connectLobbyStr
});
}
});
text_button(gui, "Paste and Connect button", "Paste & Connect", {
.wide = true,
.onClick = [&] {
connectOnPaste = true;
main.input.call_paste(CustomEvents::PasteEvent::DataType::TEXT, { .allowRichText = false });
}
});
}
}
}
});
}
void FileSelectScreen::settings_view() {
auto& gui = main.g.gui;
@ -875,6 +930,7 @@ void FileSelectScreen::settings_view() {
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
}) {
input_text_field(gui, "display name input", "Display name", &main.conf.displayName);
color_picker_button_field(gui, "defaultCanvasBackgroundColor", "Default canvas background color", &main.conf.defaultCanvasBackgroundColor, { .hasAlpha = false });
input_scalar_field(gui, "Max GUI Scale", "Max GUI Scale", &main.conf.guiScale, 1.0f, 2.0f, {
.decimalPrecision = 1,
@ -926,6 +982,15 @@ void FileSelectScreen::start_edit_mode() {
editMode = true;
}
void FileSelectScreen::input_paste_callback(const CustomEvents::PasteEvent& paste) {
if(connectOnPaste && paste.type == CustomEvents::PasteEvent::DataType::TEXT) {
CustomEvents::emit_event<CustomEvents::OpenInfiniPaintFileEvent>({
.isClient = true,
.netSource = paste.data
});
}
}
void FileSelectScreen::input_open_infinipaint_file_callback(const CustomEvents::OpenInfiniPaintFileEvent& openFile) {
main.create_new_tab(openFile);
main.set_screen([&] (std::unique_ptr<Screen>) { return std::make_unique<PhoneDrawingProgramScreen>(main); });

View file

@ -27,6 +27,7 @@ class FileSelectScreen : public Screen {
FileSelectScreen(MainProgram& m);
virtual void gui_layout_run() override;
virtual void draw(SkCanvas* canvas) override;
virtual void input_paste_callback(const CustomEvents::PasteEvent& paste) override;
virtual void input_open_infinipaint_file_callback(const CustomEvents::OpenInfiniPaintFileEvent& openFile) override;
virtual void input_global_back_button_callback() override;
virtual void input_app_about_to_go_to_background_callback() override;
@ -65,6 +66,9 @@ class FileSelectScreen : public Screen {
void save_files();
bool connectOnPaste = false;
std::string connectLobbyStr;
struct FileInfo {
std::string fileName;
SDL_Time lastModifyTime;
@ -85,6 +89,7 @@ class FileSelectScreen : public Screen {
void main_display();
void main_menu();
void file_view();
void connect_view();
void settings_view();
void create_file_button();
void file_view_edit();
@ -115,6 +120,7 @@ class FileSelectScreen : public Screen {
enum class SelectedMenu {
FILES,
TRASH,
CONNECT,
SETTINGS
} selectedMenu = SelectedMenu::FILES;

View file

@ -29,6 +29,7 @@
#include "../GUIStuff/ElementHelpers/TextLabelHelpers.hpp"
#include "../GUIStuff/ElementHelpers/TextBoxHelpers.hpp"
#include "FileSelectScreen.hpp"
#include "../NetThreadManager.hpp"
#include <Helpers/Logger.hpp>
using namespace GUIStuff;
@ -71,9 +72,21 @@ void PhoneDrawingProgramScreen::center_message(const char* id, const std::string
}
void PhoneDrawingProgramScreen::gui_layout_run() {
gui_layout_safety_checks();
main_display();
}
void PhoneDrawingProgramScreen::gui_layout_safety_checks() {
if(!main.world->netServer) {
if(topToolbarSettingsPopup == TopToolbarSettingsPopup::LOBBY_INFO)
topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE;
}
if(!main.world->netClient && !main.world->netServer) {
if(topToolbarSettingsPopup == TopToolbarSettingsPopup::PLAYER_LIST)
topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE;
}
}
void PhoneDrawingProgramScreen::main_display() {
CLAY_AUTO_ID({
.layout = {
@ -83,18 +96,22 @@ void PhoneDrawingProgramScreen::main_display() {
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
}) {
if(hideGUI)
hidden_gui();
if(main.world->netClient && main.world->clientStillConnecting)
center_message("Connecting to server message", "Connecting to server...");
else {
top_toolbar();
top_toolbar_settings_popup();
CLAY_AUTO_ID({
.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}
}) {}
bottom_toolbar();
if(hideGUI)
hidden_gui();
else {
top_toolbar();
top_toolbar_settings_popup();
CLAY_AUTO_ID({
.layout = {.sizing = {.width = CLAY_SIZING_GROW(0), .height = CLAY_SIZING_GROW(0)}}
}) {}
bottom_toolbar();
}
if(!main.world->drawProg.layerMan.is_a_layer_being_edited())
center_message("Select layer to edit message", "Select a layer to edit");
}
if(!main.world->drawProg.layerMan.is_a_layer_being_edited())
center_message("Select layer to edit message", "Select a layer to edit");
}
}
@ -241,6 +258,97 @@ void PhoneDrawingProgramScreen::top_toolbar_settings_popup() {
});
break;
}
case TopToolbarSettingsPopup::LOBBY_INFO: {
gui.set_z_index(gui.get_z_index() + 20, [&] {
gui.element<LayoutElement>("host lobby info menu", [&] (LayoutElement*, const Clay_ElementId& lId) {
Vector2f popupSize = {
std::clamp(gui.io.safeWindowRect.width() - 10.0f, 20.0f, 300.0f),
std::clamp(gui.io.safeWindowRect.height() - 40.0f, 50.0f, 600.0f)
};
CLAY(lId, {
.layout = {
.sizing = {.width = CLAY_SIZING_FIT(popupSize.x()), .height = CLAY_SIZING_FIT(0, popupSize.y()) },
.padding = CLAY_PADDING_ALL(io.theme->padding1),
.childGap = io.theme->childGap1,
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = convert_vec4<Clay_Color>(io.theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1),
.floating = {.offset = {.x = 0, .y = static_cast<float>(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT}
}) {
text_label_centered(gui, "Lobby Info");
input_text_field(gui, "lobby", "Lobby", &serverToConnectTo, {
.immutable = true
});
text_button(gui, "Copy button", "Copy", {
.wide = true,
.onClick = [&] {
main.input.set_clipboard_str(serverToConnectTo);
topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE;
}
});
}
});
});
break;
}
case TopToolbarSettingsPopup::PLAYER_LIST: {
gui.set_z_index(gui.get_z_index() + 20, [&] {
gui.element<LayoutElement>("player list menu", [&] (LayoutElement*, const Clay_ElementId& lId) {
Vector2f popupSize = {
std::clamp(gui.io.safeWindowRect.width() - 10.0f, 20.0f, 300.0f),
std::clamp(gui.io.safeWindowRect.height() - 40.0f, 50.0f, 600.0f)
};
CLAY(lId, {
.layout = {
.sizing = {.width = CLAY_SIZING_FIT(popupSize.x()), .height = CLAY_SIZING_FIT(0, popupSize.y()) },
.padding = CLAY_PADDING_ALL(io.theme->padding1),
.childGap = io.theme->childGap1,
.childAlignment = { .x = CLAY_ALIGN_X_CENTER, .y = CLAY_ALIGN_Y_TOP},
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = convert_vec4<Clay_Color>(io.theme->backColor1),
.cornerRadius = CLAY_CORNER_RADIUS(io.theme->windowCorners1),
.floating = {.offset = {.x = 0, .y = static_cast<float>(io.theme->padding1)}, .zIndex = gui.get_z_index(), .attachPoints = {.element = CLAY_ATTACH_POINT_RIGHT_TOP, .parent = CLAY_ATTACH_POINT_RIGHT_BOTTOM}, .attachTo = CLAY_ATTACH_TO_PARENT}
}) {
text_label_centered(gui, "Player List");
left_to_right_line_layout(gui, [&]() {
CLAY_AUTO_ID({
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)}
},
.backgroundColor = convert_vec4<Clay_Color>(SkColor4f{main.world->ownClientData->get_cursor_color().x(), main.world->ownClientData->get_cursor_color().y(), main.world->ownClientData->get_cursor_color().z(), 1.0f}),
.cornerRadius = CLAY_CORNER_RADIUS(3)
}) {}
ellipse_wide_paragraph_label(gui, "own player name", main.world->ownClientData->get_display_name());
});
size_t num = 0;
for(auto& client : main.world->clients->get_data()) {
if(client != main.world->ownClientData) {
gui.new_id(num++, [&] {
left_to_right_line_layout(gui, [&]() {
CLAY_AUTO_ID({
.layout = {
.sizing = {.width = CLAY_SIZING_FIXED(20), .height = CLAY_SIZING_FIXED(20)}
},
.backgroundColor = convert_vec4<Clay_Color>(SkColor4f{client->get_cursor_color().x(), client->get_cursor_color().y(), client->get_cursor_color().z(), 1.0f}),
.cornerRadius = CLAY_CORNER_RADIUS(3)
}) {}
ellipse_wide_paragraph_label(gui, "other player name", client->get_display_name());
text_button(gui, "teleport button", "Jump To", { .onClick = [&] {
main.world->drawData.cam.smooth_move_to(*main.world, client->get_cam_coords(), client->get_window_size());
topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE;
}});
});
});
}
}
}
});
});
break;
}
}
}
@ -268,9 +376,8 @@ void PhoneDrawingProgramScreen::top_toolbar() {
svg_icon_button(gui, "back exit button", "data/icons/RemixIcon/arrow-left-s-line.svg", {
.drawType = SelectableButton::DrawType::TRANSPARENT_ALL,
.onClick = [&] {
main.world->save_to_file(main.world->filePath);
save_to_file();
main.set_tab_to_close(main.world.get());
main.set_screen([&] (std::unique_ptr<Screen>) { return std::make_unique<FileSelectScreen>(main); });
}
});
top_toolbar_remaining_area();
@ -350,17 +457,24 @@ void PhoneDrawingProgramScreen::top_toolbar_remaining_area() {
break;
spaceForWorldName -= BUTTON_WIDTH;
}
input_text(gui, "world name text", &main.world->name, {
.decorations = false,
.onSelect = [&] {
fileOldPath = main.world->filePath;
},
.onDeselect = [&] {
std::filesystem::remove(fileOldPath);
std::filesystem::remove(fileOldPath.parent_path() / (fileOldPath.stem().string() + ".jpg"));
main.world->autosave_to_directory(main.world->filePath.parent_path());
}
});
if(main.world->netClient) {
CLAY_AUTO_ID({
.layout = {.sizing = {.width = CLAY_SIZING_GROW(0)}}
}) { }
}
else {
input_text(gui, "world name text", &main.world->name, {
.decorations = false,
.onSelect = [&] {
fileOldPath = main.world->filePath;
},
.onDeselect = [&] {
std::filesystem::remove(fileOldPath);
std::filesystem::remove(fileOldPath.parent_path() / (fileOldPath.stem().string() + ".jpg"));
main.world->autosave_to_directory(main.world->filePath.parent_path());
}
});
}
gui.new_id("visible buttons", [&] {
for(size_t i = 0; i < buttonFreeSpace; i++) {
gui.new_id(i, [&] {
@ -418,15 +532,37 @@ void PhoneDrawingProgramScreen::top_toolbar_hidden_button_popup(GUIStuff::Elemen
}
});
l.emplace_back(TopToolbarRemainingAreaButton{
.name = "Host",
.svgPath = "data/icons/network.svg",
.onClick = [&] {
serverLocalID = NetLibrary::get_random_server_local_id();
serverToConnectTo = NetLibrary::get_global_id() + serverLocalID;
topToolbarSettingsPopup = topToolbarSettingsPopup == TopToolbarSettingsPopup::HOST ? TopToolbarSettingsPopup::NONE : TopToolbarSettingsPopup::HOST;
}
});
if(!main.world->netClient && !main.world->netServer) {
l.emplace_back(TopToolbarRemainingAreaButton{
.name = "Host",
.svgPath = "data/icons/network.svg",
.onClick = [&] {
serverLocalID = NetLibrary::get_random_server_local_id();
serverToConnectTo = NetLibrary::get_global_id() + serverLocalID;
topToolbarSettingsPopup = topToolbarSettingsPopup == TopToolbarSettingsPopup::HOST ? TopToolbarSettingsPopup::NONE : TopToolbarSettingsPopup::HOST;
}
});
}
if(main.world->netServer) {
l.emplace_back(TopToolbarRemainingAreaButton{
.name = "Lobby Info",
.svgPath = "data/icons/network.svg",
.onClick = [&] {
topToolbarSettingsPopup = topToolbarSettingsPopup == TopToolbarSettingsPopup::LOBBY_INFO ? TopToolbarSettingsPopup::NONE : TopToolbarSettingsPopup::LOBBY_INFO;
}
});
}
if(main.world->netClient || main.world->netServer) {
l.emplace_back(TopToolbarRemainingAreaButton{
.name = "Player List",
.svgPath = "data/icons/list.svg",
.onClick = [&] {
topToolbarSettingsPopup = topToolbarSettingsPopup == TopToolbarSettingsPopup::PLAYER_LIST ? TopToolbarSettingsPopup::NONE : TopToolbarSettingsPopup::PLAYER_LIST;
}
});
}
gui.set_z_index(gui.get_z_index() + 20, [&] {
dropdown_many_element_popup_layout(gui, "top toolbar hidden button popup", {
@ -1017,15 +1153,23 @@ void PhoneDrawingProgramScreen::input_global_back_button_callback() {
main.g.gui.set_to_layout();
}
else {
main.world->save_to_file(main.world->filePath);
save_to_file();
main.set_tab_to_close(main.world.get());
main.g.gui.set_to_layout();
main.set_screen([&] (std::unique_ptr<Screen>) { return std::make_unique<FileSelectScreen>(main); });
}
}
void PhoneDrawingProgramScreen::save_to_file() {
if(!main.world->netClient)
main.world->save_to_file(main.world->filePath);
}
void PhoneDrawingProgramScreen::input_app_about_to_go_to_background_callback() {
main.world->save_to_file(main.world->filePath);
save_to_file();
}
void PhoneDrawingProgramScreen::on_tab_close() {
main.set_screen([&] (std::unique_ptr<Screen>) { return std::make_unique<FileSelectScreen>(main); });
}
void PhoneDrawingProgramScreen::color_selector(Vector4f* color, const ColorSelectorData& colorSelectorData) {

View file

@ -28,6 +28,7 @@ class PhoneDrawingProgramScreen : public DrawingProgramScreen {
virtual void gui_layout_run() override;
virtual void input_global_back_button_callback() override;
virtual void input_app_about_to_go_to_background_callback() override;
virtual void on_tab_close() override;
struct ColorSelectorData {
std::function<void()> onChange;
std::function<void()> onSelect;
@ -73,7 +74,9 @@ class PhoneDrawingProgramScreen : public DrawingProgramScreen {
BOOKMARKS,
LAYERS,
GRIDS,
HOST
HOST,
LOBBY_INFO,
PLAYER_LIST
} topToolbarSettingsPopup = TopToolbarSettingsPopup::NONE;
bool hideGUI = false;
@ -82,6 +85,7 @@ class PhoneDrawingProgramScreen : public DrawingProgramScreen {
Vector4f* colorPickerPtr = nullptr;
ColorSelectorData colorPickerData;
void gui_layout_safety_checks();
void hidden_gui();
void center_message(const char* id, const std::string& m);
void default_bottom_toolbar();
@ -95,6 +99,7 @@ class PhoneDrawingProgramScreen : public DrawingProgramScreen {
void bottom_toolbar();
void main_display();
void tool_settings_popup();
void save_to_file();
std::string serverToConnectTo;
std::string serverLocalID;

View file

@ -83,6 +83,8 @@ void Screen::input_finger_motion_callback(const InputManager::FingerMotionCallba
void Screen::input_window_resize_callback(const InputManager::WindowResizeCallbackArgs& w) {}
void Screen::input_window_scale_callback(const InputManager::WindowScaleCallbackArgs& w) {}
void Screen::input_app_about_to_go_to_background_callback() {}
void Screen::input_app_about_to_go_to_foreground_callback() {}
void Screen::on_tab_close() {}
std::optional<InputManager::TextBoxStartInfo> Screen::get_text_box_start_info() { return std::nullopt; }
Screen::~Screen() {}

View file

@ -65,6 +65,8 @@ class Screen {
virtual void input_window_resize_callback(const InputManager::WindowResizeCallbackArgs& w);
virtual void input_window_scale_callback(const InputManager::WindowScaleCallbackArgs& w);
virtual void input_app_about_to_go_to_background_callback();
virtual void input_app_about_to_go_to_foreground_callback();
virtual void on_tab_close();
virtual std::optional<InputManager::TextBoxStartInfo> get_text_box_start_info();
virtual void open_file_selector(const std::string& filePickerName, const std::vector<ExtensionFilter>& extensionFilters, OpenFileSelectorCallback postSelectionFunc, const std::string& fileName = "", bool isSaving = false);

View file

@ -1601,8 +1601,11 @@ void Toolbar::general_settings_inner_gui() {
switch(generalSettingsOptions) {
case GSETTINGS_GENERAL: {
general_scroll_area("general settings", [&] {
input_text_field(gui, "display name input", "Display name", &main.conf.displayName);
main.update_display_names();
input_text_field(gui, "display name input", "Display name", &main.conf.displayName, {
.onEdit = [&] {
main.update_display_names();
}
});
color_picker_button_field(gui, "defaultCanvasBackgroundColor", "Default canvas background color", &main.conf.defaultCanvasBackgroundColor, { .hasAlpha = false });
#ifndef __EMSCRIPTEN__
checkbox_boolean_field(gui, "native file pick", "Use native file picker", &main.conf.useNativeFilePicker);

View file

@ -227,6 +227,7 @@ bool World::connection_update() {
Logger::get().log("USERINFO", "Client connection failed");
netObjMan.disconnect();
main.set_tab_to_close(this);
netClient = nullptr;
return false;
}
netClient->update();

View file

@ -670,6 +670,10 @@ SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
Logger::get().log("INFO", "[SDL_AppEvent] Entering background");
mS.m->input_app_about_to_go_to_background_callback();
break;
case SDL_EVENT_WILL_ENTER_FOREGROUND:
Logger::get().log("INFO", "[SDL_AppEvent] Entering foreground");
mS.m->input_app_about_to_go_to_foreground_callback();
break;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if(event->window.windowID == SDL_GetWindowID(mS.window)) {
if(mS.m->app_close_requested())