refactor: rebuild desktop client with Slint
@@ -1,24 +1,12 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_STATE_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#if _WIN32
|
||||
#include "platform/tray/win_tray.h"
|
||||
#elif defined(__APPLE__)
|
||||
#include "platform/tray/mac_tray.h"
|
||||
#elif defined(__linux__)
|
||||
#include "platform/tray/linux_tray.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
|
||||
struct MainWindowState {
|
||||
@@ -27,25 +15,10 @@ struct MainWindowState {
|
||||
float title_bar_button_width_ = 30;
|
||||
float title_bar_button_height_ = 30;
|
||||
|
||||
SDL_Window *main_window_ = nullptr;
|
||||
SDL_Renderer *main_renderer_ = nullptr;
|
||||
ImGuiContext *main_ctx_ = nullptr;
|
||||
ImFont *main_windows_system_chinese_font_ = nullptr;
|
||||
ImFont *stream_windows_system_chinese_font_ = nullptr;
|
||||
ImFont *server_windows_system_chinese_font_ = nullptr;
|
||||
bool exit_ = false;
|
||||
const int sdl_refresh_ms_ = 16;
|
||||
#if _WIN32
|
||||
std::unique_ptr<WinTray> tray_;
|
||||
#elif defined(__APPLE__)
|
||||
std::unique_ptr<MacTray> tray_;
|
||||
#elif defined(__linux__)
|
||||
std::unique_ptr<LinuxTray> tray_;
|
||||
#endif
|
||||
|
||||
bool main_window_minimized_ = false;
|
||||
uint32_t last_main_minimize_request_tick_ = 0;
|
||||
uint32_t last_stream_minimize_request_tick_ = 0;
|
||||
int main_window_width_real_ = 720;
|
||||
int main_window_height_real_ = 540;
|
||||
float main_window_dpi_scaling_w_ = 1.0f;
|
||||
@@ -77,8 +50,6 @@ struct MainWindowState {
|
||||
float about_window_height_ = 170;
|
||||
float update_notification_window_width_ = 400;
|
||||
float update_notification_window_height_ = 320;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
uint32_t APP_EXIT_EVENT = 0;
|
||||
};
|
||||
|
||||
// Application-global interaction flags that coordinate SDL events with media
|
||||
@@ -116,14 +87,15 @@ struct InteractionState {
|
||||
std::string controlled_remote_id_;
|
||||
std::string focused_remote_id_;
|
||||
std::string remote_client_id_;
|
||||
SDL_Event last_mouse_event{};
|
||||
};
|
||||
|
||||
struct UpdateState {
|
||||
nlohmann::json latest_version_info_ = nlohmann::json{};
|
||||
bool update_available_ = false;
|
||||
std::string latest_version_;
|
||||
std::string release_name_;
|
||||
std::string release_notes_;
|
||||
std::string release_date_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
@@ -131,20 +103,15 @@ struct UpdateState {
|
||||
};
|
||||
|
||||
struct StreamWindowState {
|
||||
SDL_Window *stream_window_ = nullptr;
|
||||
SDL_Renderer *stream_renderer_ = nullptr;
|
||||
ImGuiContext *stream_ctx_ = nullptr;
|
||||
bool need_to_create_stream_window_ = false;
|
||||
bool stream_window_created_ = false;
|
||||
bool stream_window_inited_ = false;
|
||||
bool window_maximized_ = false;
|
||||
bool stream_window_grabbed_ = false;
|
||||
bool control_mouse_ = false;
|
||||
int stream_window_width_default_ = 1280;
|
||||
int stream_window_height_default_ = 720;
|
||||
float stream_window_width_ = 1280;
|
||||
float stream_window_height_ = 720;
|
||||
SDL_PixelFormat stream_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int stream_window_width_real_ = 1280;
|
||||
int stream_window_height_real_ = 720;
|
||||
float stream_window_dpi_scaling_w_ = 1.0f;
|
||||
@@ -152,9 +119,6 @@ struct StreamWindowState {
|
||||
};
|
||||
|
||||
struct ServerWindowState {
|
||||
SDL_Window *server_window_ = nullptr;
|
||||
SDL_Renderer *server_renderer_ = nullptr;
|
||||
ImGuiContext *server_ctx_ = nullptr;
|
||||
bool need_to_create_server_window_ = false;
|
||||
bool need_to_destroy_server_window_ = false;
|
||||
bool server_window_created_ = false;
|
||||
@@ -164,7 +128,6 @@ struct ServerWindowState {
|
||||
float server_window_width_ = 250;
|
||||
float server_window_height_ = 150;
|
||||
float server_window_title_bar_height_ = 30.0f;
|
||||
SDL_PixelFormat server_pixformat_ = SDL_PIXELFORMAT_NV12;
|
||||
int server_window_normal_width_ = 250;
|
||||
int server_window_normal_height_ = 150;
|
||||
float server_window_dpi_scaling_w_ = 1.0f;
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
#ifndef CROSSDESK_GUI_APPLICATION_H_
|
||||
#define CROSSDESK_GUI_APPLICATION_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "IconsFontAwesome6.h"
|
||||
#include "imgui_impl_sdl3.h"
|
||||
#include "imgui_impl_sdlrenderer3.h"
|
||||
#include "imgui_internal.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// SDL/ImGui application shell. Feature state and protocol behavior live in
|
||||
// GuiRuntime and its composed controllers.
|
||||
// Slint application shell. GuiRuntime remains the owner of transport, media,
|
||||
// device, clipboard, transfer and settings behavior.
|
||||
class GuiApplication final : private GuiRuntime {
|
||||
public:
|
||||
GuiApplication();
|
||||
@@ -23,88 +18,47 @@ public:
|
||||
int Run();
|
||||
|
||||
private:
|
||||
// Native window integration.
|
||||
static SDL_HitTestResult HitTestCallback(SDL_Window *window,
|
||||
const SDL_Point *area, void *data);
|
||||
struct SlintUi;
|
||||
|
||||
// Application lifecycle.
|
||||
void InitializeLogger();
|
||||
void InitializeSettings();
|
||||
void InitializeSDL();
|
||||
bool InitializeSDL();
|
||||
void InitializeModules();
|
||||
void InitializeMainWindow();
|
||||
void MainLoop();
|
||||
void HandleStreamWindow();
|
||||
void HandleServerWindow();
|
||||
void Cleanup();
|
||||
|
||||
// Window lifecycle and rendering.
|
||||
int CreateMainWindow();
|
||||
int DestroyMainWindow();
|
||||
int CreateStreamWindow();
|
||||
int DestroyStreamWindow();
|
||||
int CreateServerWindow();
|
||||
int DestroyServerWindow();
|
||||
int SetupFontAndStyle(ImFont **system_chinese_font_out);
|
||||
int DestroyMainWindowContext();
|
||||
int DestroyStreamWindowContext();
|
||||
int DestroyServerWindowContext();
|
||||
int DrawMainWindow();
|
||||
int DrawStreamWindow();
|
||||
int DrawServerWindow();
|
||||
|
||||
// Views and dialogs.
|
||||
int TitleBar(bool main_window);
|
||||
int MainWindow();
|
||||
int UpdateNotificationWindow();
|
||||
int StreamWindow();
|
||||
int ServerWindow();
|
||||
int RemoteClientInfoWindow();
|
||||
int LocalWindow();
|
||||
int RemoteWindow();
|
||||
int RecentConnectionsWindow();
|
||||
int SettingWindow();
|
||||
int SelfHostedServerWindow();
|
||||
int ControlWindow(std::shared_ptr<RemoteSession> &props);
|
||||
int ControlBar(std::shared_ptr<RemoteSession> &props);
|
||||
int AboutWindow();
|
||||
int StatusBar();
|
||||
bool
|
||||
ConnectionStatusWindow(std::shared_ptr<RemoteSession> &props);
|
||||
int ShowRecentConnections();
|
||||
int ConfirmDeleteConnection();
|
||||
int EditRecentConnectionAliasWindow();
|
||||
int OfflineWarningWindow();
|
||||
int NetTrafficStats(std::shared_ptr<RemoteSession> &props);
|
||||
int FileTransferWindow(std::shared_ptr<RemoteSession> &props);
|
||||
void
|
||||
DrawConnectionStatusText(std::shared_ptr<RemoteSession> &props);
|
||||
void
|
||||
DrawReceivingScreenText(std::shared_ptr<RemoteSession> &props);
|
||||
bool OpenUrl(const std::string &url);
|
||||
void Hyperlink(const std::string &label, const std::string &url,
|
||||
float window_width);
|
||||
std::string OpenFileDialog(std::string title);
|
||||
void InitializeUi();
|
||||
void InitializeSystemTray();
|
||||
bool MinimizeMainWindowToTray();
|
||||
|
||||
// SDL event dispatch and input translation.
|
||||
void UpdateRenderRect();
|
||||
void ProcessSdlEvent(const SDL_Event &event);
|
||||
int ProcessKeyboardEvent(const SDL_Event &event);
|
||||
int ProcessMouseEvent(const SDL_Event &event);
|
||||
void CloseTab(decltype(remote_sessions_)::iterator &it);
|
||||
|
||||
void BindMainCallbacks();
|
||||
void BindStreamCallbacks();
|
||||
void BindServerCallbacks();
|
||||
void Tick();
|
||||
void SyncMainWindow();
|
||||
void SyncConnectionDialog();
|
||||
void SyncPlatformDialogs();
|
||||
void SyncStreamWindow();
|
||||
void SyncServerWindow();
|
||||
void UpdateLocalization();
|
||||
void ResetSettingsUi();
|
||||
void SaveSettingsFromUi();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
void CheckPortableWindowsService();
|
||||
int PortableServiceInstallWindow();
|
||||
void StartPortableWindowsServiceInstall();
|
||||
void JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
void ConnectFromUi(const std::string &remote_id);
|
||||
void SelectStreamTab(int index);
|
||||
void ReorderStreamTab(int from, float drop_x, float tab_width);
|
||||
void CloseStreamTab(const std::string &remote_id);
|
||||
void SendPointerInput(int button, int kind, float x, float y);
|
||||
void SendScrollInput(float delta_x, float delta_y, float x, float y);
|
||||
void SendKeyInput(const std::string &text, bool pressed, bool control,
|
||||
bool alt, bool shift, bool meta);
|
||||
void Cleanup();
|
||||
|
||||
#ifdef __APPLE__
|
||||
int RequestPermissionWindow();
|
||||
bool DrawToggleSwitch(const char *id, bool active, bool enabled);
|
||||
#endif
|
||||
std::shared_ptr<RemoteSession> SelectedSession();
|
||||
std::string OpenFileDialog(const std::string &title);
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "service_host.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path GetCurrentExecutablePath() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
const DWORD length = GetModuleFileNameW(
|
||||
nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
if (length < buffer.size()) {
|
||||
return std::filesystem::path(buffer.data(), buffer.data() + length);
|
||||
}
|
||||
if (buffer.size() >= 32768) {
|
||||
return {};
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool InstallServiceWithElevation() {
|
||||
const std::filesystem::path executable_path = GetCurrentExecutablePath();
|
||||
if (executable_path.empty()) {
|
||||
LOG_ERROR("Portable service install failed: current executable not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path service_path =
|
||||
executable_path.parent_path() / L"crossdesk_service.exe";
|
||||
const std::filesystem::path helper_path =
|
||||
executable_path.parent_path() / L"crossdesk_session_helper.exe";
|
||||
if (!std::filesystem::exists(service_path) ||
|
||||
!std::filesystem::exists(helper_path)) {
|
||||
LOG_ERROR("Portable service install failed: service binaries missing, "
|
||||
"service={}, helper={}",
|
||||
service_path.string(), helper_path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring executable = executable_path.wstring();
|
||||
const std::wstring working_dir = executable_path.parent_path().wstring();
|
||||
constexpr wchar_t parameters[] = L"--service-install";
|
||||
|
||||
SHELLEXECUTEINFOW execute_info{};
|
||||
execute_info.cbSize = sizeof(execute_info);
|
||||
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
execute_info.lpVerb = L"runas";
|
||||
execute_info.lpFile = executable.c_str();
|
||||
execute_info.lpParameters = parameters;
|
||||
execute_info.lpDirectory = working_dir.c_str();
|
||||
execute_info.nShow = SW_HIDE;
|
||||
|
||||
if (!ShellExecuteExW(&execute_info)) {
|
||||
LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
const DWORD wait_result =
|
||||
WaitForSingleObject(execute_info.hProcess, INFINITE);
|
||||
DWORD exit_code = 1;
|
||||
if (wait_result == WAIT_OBJECT_0) {
|
||||
GetExitCodeProcess(execute_info.hProcess, &exit_code);
|
||||
} else {
|
||||
LOG_ERROR("Portable service install wait failed, result={}", wait_result);
|
||||
}
|
||||
CloseHandle(execute_info.hProcess);
|
||||
|
||||
if (exit_code != 0) {
|
||||
LOG_ERROR("Portable service install command failed, exit_code={}",
|
||||
exit_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool started = StartCrossDeskService();
|
||||
if (!started) {
|
||||
LOG_WARN("Portable service installed but start failed");
|
||||
}
|
||||
return IsCrossDeskServiceInstalled() && started;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void GuiApplication::CheckPortableWindowsService() {
|
||||
if (portable_service_prompt_checked_) {
|
||||
return;
|
||||
}
|
||||
portable_service_prompt_checked_ = true;
|
||||
portable_service_installed_ = IsCrossDeskServiceInstalled();
|
||||
if (portable_service_installed_ || portable_service_prompt_suppressed_) {
|
||||
return;
|
||||
}
|
||||
|
||||
portable_service_install_state_.store(PortableServiceInstallState::idle,
|
||||
std::memory_order_relaxed);
|
||||
show_portable_service_install_window_ = true;
|
||||
}
|
||||
|
||||
void GuiApplication::StartPortableWindowsServiceInstall() {
|
||||
portable_service_do_not_remind_ = false;
|
||||
PortableServiceInstallState expected = PortableServiceInstallState::idle;
|
||||
if (!portable_service_install_state_.compare_exchange_strong(
|
||||
expected, PortableServiceInstallState::installing,
|
||||
std::memory_order_acq_rel)) {
|
||||
if (expected != PortableServiceInstallState::failed) {
|
||||
return;
|
||||
}
|
||||
portable_service_install_state_.store(
|
||||
PortableServiceInstallState::installing, std::memory_order_release);
|
||||
}
|
||||
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
portable_service_install_thread_ = std::thread([this]() {
|
||||
const bool installed = InstallServiceWithElevation();
|
||||
portable_service_install_state_.store(
|
||||
installed ? PortableServiceInstallState::succeeded
|
||||
: PortableServiceInstallState::failed,
|
||||
std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
void GuiApplication::JoinPortableWindowsServiceInstallThread() {
|
||||
if (portable_service_install_thread_.joinable()) {
|
||||
portable_service_install_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -58,7 +58,7 @@ using TranslationTable =
|
||||
inline std::unordered_map<std::string, std::string> MakeLocalizedValues(
|
||||
const TranslationRow& row) {
|
||||
return {{"zh-CN", reinterpret_cast<const char*>(row.zh)},
|
||||
{"en-US", row.en},
|
||||
{"en-US", reinterpret_cast<const char*>(row.en)},
|
||||
{"ru-RU", reinterpret_cast<const char*>(row.ru)}};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ namespace detail {
|
||||
|
||||
struct TranslationRow {
|
||||
const char* key;
|
||||
const char* zh;
|
||||
const char* en;
|
||||
const char* ru;
|
||||
const void* zh;
|
||||
const void* en;
|
||||
const void* ru;
|
||||
};
|
||||
|
||||
// Single source of truth for all UI strings.
|
||||
|
||||
@@ -33,11 +33,6 @@ void ClipboardController::Initialize() {
|
||||
SDL_free(clipboard_text);
|
||||
}
|
||||
|
||||
if (event_type_ == 0) {
|
||||
LOG_ERROR("Clipboard synchronization disabled: SDL event is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
events_enabled_.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
@@ -75,7 +70,7 @@ void ClipboardController::QueueRemoteText(const char *data, size_t size) {
|
||||
|
||||
SDL_Event event{};
|
||||
event.type = event_type_;
|
||||
if (!SDL_PushEvent(&event)) {
|
||||
if (event_type_ != 0 && !SDL_PushEvent(&event)) {
|
||||
// MainLoop also drains the pending value after its wait timeout.
|
||||
LOG_WARN("Failed to wake SDL loop for remote clipboard text: {}",
|
||||
SDL_GetError());
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "file_transfer.h"
|
||||
#include "filesystem_utf8.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
@@ -40,7 +41,7 @@ void FileTransferManager::ProcessSelectedFile(
|
||||
FileTransferState &state = state_for(props);
|
||||
LOG_INFO("Selected file: {}", path.c_str());
|
||||
|
||||
const std::filesystem::path file_path = std::filesystem::u8path(path);
|
||||
const std::filesystem::path file_path = PathFromUtf8(path);
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_ERROR("Selected path is not a regular file: {}", path);
|
||||
@@ -55,7 +56,9 @@ void FileTransferManager::ProcessSelectedFile(
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.file_transfer_list_mutex_);
|
||||
FileTransferState::FileTransferInfo info;
|
||||
info.file_name = file_path.filename().u8string();
|
||||
const auto utf8_name = file_path.filename().u8string();
|
||||
info.file_name.assign(reinterpret_cast<const char *>(utf8_name.data()),
|
||||
utf8_name.size());
|
||||
info.file_path = file_path;
|
||||
info.file_size = file_size;
|
||||
info.status = FileTransferState::FileTransferStatus::Queued;
|
||||
@@ -341,39 +344,4 @@ void FileTransferManager::HandleAck(const char *data, size_t size) {
|
||||
ProcessQueue(props);
|
||||
}
|
||||
|
||||
void FileTransferManager::HandleDropEvent(const SDL_Event &event) {
|
||||
if (event.type != SDL_EVENT_DROP_FILE ||
|
||||
!((owner_.stream_window_ &&
|
||||
SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) ||
|
||||
(owner_.server_window_ &&
|
||||
SDL_GetWindowID(owner_.server_window_) == event.window.windowID))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (owner_.stream_window_ &&
|
||||
SDL_GetWindowID(owner_.stream_window_) == event.window.windowID) {
|
||||
if (!owner_.stream_window_inited_ || !event.drop.data) {
|
||||
return;
|
||||
}
|
||||
std::shared_lock lock(owner_.remote_sessions_mutex_);
|
||||
for (const auto &[remote_id, props] : owner_.remote_sessions_) {
|
||||
if (props && props->tab_selected_ && props->peer_) {
|
||||
ProcessSelectedFile(static_cast<const char *>(event.drop.data), props,
|
||||
props->file_label_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (owner_.server_window_inited_ && event.drop.data) {
|
||||
const auto file_path = std::filesystem::u8path(event.drop.data);
|
||||
std::error_code ec;
|
||||
if (std::filesystem::is_regular_file(file_path, ec)) {
|
||||
LOG_INFO("Drop file [{}] on server window (size: {} bytes)",
|
||||
event.drop.data, std::filesystem::file_size(file_path, ec));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#ifndef CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
#define CROSSDESK_GUI_FILE_TRANSFER_MANAGER_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
@@ -33,7 +31,6 @@ public:
|
||||
const std::shared_ptr<RemoteSession> &props,
|
||||
const std::string &file_label,
|
||||
const std::string &remote_id = "");
|
||||
void HandleDropEvent(const SDL_Event &event);
|
||||
void HandleAck(const char *data, size_t size);
|
||||
|
||||
private:
|
||||
|
||||
@@ -190,6 +190,18 @@ struct LinuxTrayImpl {
|
||||
menu_label(GetMenuLabel(language_index_value)),
|
||||
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
|
||||
|
||||
explicit LinuxTrayImpl(std::function<void()> show_window_callback,
|
||||
std::function<void()> hide_window_callback,
|
||||
std::function<void()> exit_callback,
|
||||
std::string tray_tooltip, int language_index_value)
|
||||
: show_window(std::move(show_window_callback)),
|
||||
hide_window(std::move(hide_window_callback)),
|
||||
exit_app(std::move(exit_callback)),
|
||||
tooltip(std::move(tray_tooltip)),
|
||||
language_index(language_index_value),
|
||||
menu_label(GetMenuLabel(language_index_value)),
|
||||
menu_ascii_label(IsAsciiPrintable(menu_label) ? menu_label : "Exit") {}
|
||||
|
||||
~LinuxTrayImpl() { RemoveTrayIcon(); }
|
||||
|
||||
bool MinimizeToTray() {
|
||||
@@ -197,7 +209,9 @@ struct LinuxTrayImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (app_window) {
|
||||
if (hide_window) {
|
||||
hide_window();
|
||||
} else if (app_window) {
|
||||
SDL_HideWindow(app_window);
|
||||
}
|
||||
return true;
|
||||
@@ -939,16 +953,20 @@ struct LinuxTrayImpl {
|
||||
}
|
||||
|
||||
void ShowWindow() {
|
||||
if (!app_window) {
|
||||
return;
|
||||
if (show_window) {
|
||||
show_window();
|
||||
} else if (app_window) {
|
||||
SDL_ShowWindow(app_window);
|
||||
SDL_RestoreWindow(app_window);
|
||||
SDL_RaiseWindow(app_window);
|
||||
}
|
||||
|
||||
SDL_ShowWindow(app_window);
|
||||
SDL_RestoreWindow(app_window);
|
||||
SDL_RaiseWindow(app_window);
|
||||
}
|
||||
|
||||
void RequestExit() {
|
||||
if (exit_app) {
|
||||
exit_app();
|
||||
return;
|
||||
}
|
||||
SDL_Event event{};
|
||||
event.type =
|
||||
exit_event_type == 0 || exit_event_type == static_cast<uint32_t>(-1)
|
||||
@@ -958,6 +976,9 @@ struct LinuxTrayImpl {
|
||||
}
|
||||
|
||||
::SDL_Window* app_window = nullptr;
|
||||
std::function<void()> show_window;
|
||||
std::function<void()> hide_window;
|
||||
std::function<void()> exit_app;
|
||||
std::string tooltip;
|
||||
int language_index = 0;
|
||||
uint32_t exit_event_type = 0;
|
||||
@@ -1005,6 +1026,14 @@ LinuxTray::LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
: impl_(std::make_unique<LinuxTrayImpl>(app_window, tooltip, language_index,
|
||||
exit_event_type)) {}
|
||||
|
||||
LinuxTray::LinuxTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> exit_app,
|
||||
const std::string& tooltip, int language_index)
|
||||
: impl_(std::make_unique<LinuxTrayImpl>(
|
||||
std::move(show_window), std::move(hide_window), std::move(exit_app),
|
||||
tooltip, language_index)) {}
|
||||
|
||||
LinuxTray::~LinuxTray() = default;
|
||||
|
||||
bool LinuxTray::MinimizeToTray() { return impl_->MinimizeToTray(); }
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -23,6 +24,9 @@ class LinuxTray {
|
||||
public:
|
||||
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
int language_index, uint32_t exit_event_type);
|
||||
LinuxTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window, std::function<void()> exit_app,
|
||||
const std::string& tooltip, int language_index);
|
||||
~LinuxTray();
|
||||
|
||||
bool MinimizeToTray();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#ifndef _MAC_TRAY_H_
|
||||
#define _MAC_TRAY_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -20,6 +21,11 @@ class MacTray {
|
||||
public:
|
||||
MacTray(::SDL_Window* app_window, const std::string& tooltip,
|
||||
int language_index);
|
||||
MacTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> open_settings,
|
||||
std::function<void()> exit_app,
|
||||
const std::string& tooltip, int language_index);
|
||||
~MacTray();
|
||||
|
||||
void MinimizeToTray();
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@interface CrossDeskMacTrayTarget : NSObject
|
||||
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
|
||||
- (void)statusItemClicked:(id)sender;
|
||||
- (void)openSettings:(id)sender;
|
||||
- (void)exitApplication:(id)sender;
|
||||
@end
|
||||
|
||||
@@ -24,7 +25,24 @@ struct MacTrayImpl {
|
||||
: app_window(window),
|
||||
tooltip(std::move(tray_tooltip)),
|
||||
language_index(language_index_value),
|
||||
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
|
||||
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {
|
||||
EnsureStatusItem();
|
||||
}
|
||||
|
||||
explicit MacTrayImpl(std::function<void()> show_window_callback,
|
||||
std::function<void()> hide_window_callback,
|
||||
std::function<void()> open_settings_callback,
|
||||
std::function<void()> exit_callback,
|
||||
std::string tray_tooltip, int language_index_value)
|
||||
: show_window(std::move(show_window_callback)),
|
||||
hide_window(std::move(hide_window_callback)),
|
||||
open_settings(std::move(open_settings_callback)),
|
||||
exit_app(std::move(exit_callback)),
|
||||
tooltip(std::move(tray_tooltip)),
|
||||
language_index(language_index_value),
|
||||
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {
|
||||
EnsureStatusItem();
|
||||
}
|
||||
|
||||
~MacTrayImpl() {
|
||||
RemoveTrayIcon();
|
||||
@@ -33,7 +51,9 @@ struct MacTrayImpl {
|
||||
|
||||
void MinimizeToTray() {
|
||||
EnsureStatusItem();
|
||||
if (app_window) {
|
||||
if (hide_window) {
|
||||
hide_window();
|
||||
} else if (app_window) {
|
||||
SDL_HideWindow(app_window);
|
||||
}
|
||||
}
|
||||
@@ -48,12 +68,12 @@ struct MacTrayImpl {
|
||||
}
|
||||
|
||||
void ShowWindow() {
|
||||
if (!app_window) {
|
||||
return;
|
||||
if (show_window) {
|
||||
show_window();
|
||||
} else if (app_window) {
|
||||
SDL_ShowWindow(app_window);
|
||||
SDL_RaiseWindow(app_window);
|
||||
}
|
||||
|
||||
SDL_ShowWindow(app_window);
|
||||
SDL_RaiseWindow(app_window);
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
|
||||
@@ -64,6 +84,18 @@ struct MacTrayImpl {
|
||||
}
|
||||
|
||||
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
|
||||
NSString *settings_title =
|
||||
NSStringFromUtf8(localization::settings
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_index)]);
|
||||
NSMenuItem *settings_item =
|
||||
[[NSMenuItem alloc] initWithTitle:settings_title
|
||||
action:@selector(openSettings:)
|
||||
keyEquivalent:@""];
|
||||
[settings_item setTarget:target];
|
||||
[menu addItem:settings_item];
|
||||
[menu addItem:[NSMenuItem separatorItem]];
|
||||
|
||||
NSString *exit_title =
|
||||
NSStringFromUtf8(localization::exit_program
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
@@ -86,11 +118,25 @@ struct MacTrayImpl {
|
||||
}
|
||||
|
||||
void RequestExit() {
|
||||
if (exit_app) {
|
||||
exit_app();
|
||||
return;
|
||||
}
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
void OpenSettings() {
|
||||
if (open_settings) {
|
||||
open_settings();
|
||||
} else {
|
||||
ShowWindow();
|
||||
return;
|
||||
}
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
}
|
||||
|
||||
private:
|
||||
void EnsureStatusItem() {
|
||||
if (status_item) {
|
||||
@@ -191,6 +237,10 @@ struct MacTrayImpl {
|
||||
}
|
||||
|
||||
::SDL_Window *app_window = nullptr;
|
||||
std::function<void()> show_window;
|
||||
std::function<void()> hide_window;
|
||||
std::function<void()> open_settings;
|
||||
std::function<void()> exit_app;
|
||||
std::string tooltip;
|
||||
int language_index = 0;
|
||||
NSStatusItem *status_item = nil;
|
||||
@@ -202,6 +252,16 @@ MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
|
||||
: impl_(
|
||||
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
|
||||
|
||||
MacTray::MacTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> open_settings,
|
||||
std::function<void()> exit_app, const std::string &tooltip,
|
||||
int language_index)
|
||||
: impl_(std::make_unique<MacTrayImpl>(
|
||||
std::move(show_window), std::move(hide_window),
|
||||
std::move(open_settings), std::move(exit_app), tooltip,
|
||||
language_index)) {}
|
||||
|
||||
MacTray::~MacTray() = default;
|
||||
|
||||
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
|
||||
@@ -244,6 +304,13 @@ void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
|
||||
}
|
||||
}
|
||||
|
||||
- (void)openSettings:(id)sender {
|
||||
(void)sender;
|
||||
if (owner_) {
|
||||
owner_->OpenSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#endif // __APPLE__
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "localization.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// callback for the message-only window that handles tray icon messages
|
||||
@@ -60,6 +62,16 @@ WinTray::WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
|
||||
wcsncpy_s(nid_.szTip, tip_.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
WinTray::WinTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window,
|
||||
std::function<void()> exit_app, HICON icon,
|
||||
const std::wstring& tooltip, int language_index)
|
||||
: WinTray(nullptr, icon, tooltip, language_index) {
|
||||
show_window_ = std::move(show_window);
|
||||
hide_window_ = std::move(hide_window);
|
||||
exit_app_ = std::move(exit_app);
|
||||
}
|
||||
|
||||
WinTray::~WinTray() {
|
||||
RemoveTrayIcon();
|
||||
if (hwnd_message_only_) DestroyWindow(hwnd_message_only_);
|
||||
@@ -67,20 +79,31 @@ WinTray::~WinTray() {
|
||||
|
||||
void WinTray::MinimizeToTray() {
|
||||
Shell_NotifyIcon(NIM_ADD, &nid_);
|
||||
// hide application window
|
||||
ShowWindow(app_hwnd_, SW_HIDE);
|
||||
if (hide_window_) {
|
||||
hide_window_();
|
||||
} else if (app_hwnd_) {
|
||||
ShowWindow(app_hwnd_, SW_HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
void WinTray::RemoveTrayIcon() { Shell_NotifyIcon(NIM_DELETE, &nid_); }
|
||||
|
||||
void WinTray::ShowApplicationWindow() {
|
||||
if (show_window_) {
|
||||
show_window_();
|
||||
} else if (app_hwnd_) {
|
||||
ShowWindow(app_hwnd_, SW_SHOW);
|
||||
SetForegroundWindow(app_hwnd_);
|
||||
}
|
||||
}
|
||||
|
||||
bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
if (!msg || msg->message != WM_TRAY_CALLBACK) return false;
|
||||
|
||||
switch (LOWORD(msg->lParam)) {
|
||||
case WM_LBUTTONDBLCLK:
|
||||
case WM_LBUTTONUP: {
|
||||
ShowWindow(app_hwnd_, SW_SHOW);
|
||||
SetForegroundWindow(app_hwnd_);
|
||||
ShowApplicationWindow();
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -99,13 +122,15 @@ bool WinTray::HandleTrayMessage(MSG* msg) {
|
||||
|
||||
// handle menu command
|
||||
if (cmd == 1001) {
|
||||
// exit application
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
if (exit_app_) {
|
||||
exit_app_();
|
||||
} else {
|
||||
SDL_Event event;
|
||||
event.type = SDL_EVENT_QUIT;
|
||||
SDL_PushEvent(&event);
|
||||
}
|
||||
} else if (cmd == 1002) {
|
||||
ShowWindow(app_hwnd_, SW_SHOW); // show main window
|
||||
SetForegroundWindow(app_hwnd_);
|
||||
ShowApplicationWindow();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#define WM_TRAY_CALLBACK (WM_USER + 1)
|
||||
@@ -20,6 +21,9 @@ class WinTray {
|
||||
public:
|
||||
WinTray(HWND app_hwnd, HICON icon, const std::wstring& tooltip,
|
||||
int language_index);
|
||||
WinTray(std::function<void()> show_window,
|
||||
std::function<void()> hide_window, std::function<void()> exit_app,
|
||||
HICON icon, const std::wstring& tooltip, int language_index);
|
||||
~WinTray();
|
||||
|
||||
void MinimizeToTray();
|
||||
@@ -27,12 +31,17 @@ class WinTray {
|
||||
bool HandleTrayMessage(MSG* msg);
|
||||
|
||||
private:
|
||||
void ShowApplicationWindow();
|
||||
|
||||
HWND app_hwnd_;
|
||||
HWND hwnd_message_only_;
|
||||
HICON icon_;
|
||||
std::wstring tip_;
|
||||
int language_index_;
|
||||
NOTIFYICONDATA nid_;
|
||||
std::function<void()> show_window_;
|
||||
std::function<void()> hide_window_;
|
||||
std::function<void()> exit_app_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Starts the platform's native move loop for the window receiving the current
|
||||
// left-mouse-down event. Returns false when no suitable native event exists.
|
||||
bool StartNativeWindowDrag();
|
||||
|
||||
// Hides the disabled native zoom button on CrossDesk's fixed-size main
|
||||
// window. Returns true once the matching AppKit window has been configured.
|
||||
bool HideDisabledMainWindowZoomButton();
|
||||
|
||||
// Configures live resize and replaces the stream window's native Space
|
||||
// fullscreen action with an immediate, single-window fullscreen transition.
|
||||
bool ConfigureStreamWindowLiveResize();
|
||||
|
||||
// Enters or leaves the stream window's animation-free fullscreen mode.
|
||||
bool SetStreamWindowFullscreen(bool fullscreen);
|
||||
bool IsStreamWindowFullscreen();
|
||||
|
||||
// Opens an application-modal macOS file picker. The panel is made key before
|
||||
// entering its modal loop so it receives keyboard input immediately.
|
||||
std::string OpenNativeFileDialog(const std::string &title);
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,265 @@
|
||||
#include "platform/window_drag.h"
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
@interface CrossDeskStreamFullscreenTarget : NSObject
|
||||
- (void)toggleStreamFullscreen:(id)sender;
|
||||
@end
|
||||
|
||||
@implementation CrossDeskStreamFullscreenTarget
|
||||
- (void)toggleStreamFullscreen:(id)sender {
|
||||
(void)sender;
|
||||
crossdesk::SetStreamWindowFullscreen(
|
||||
!crossdesk::IsStreamWindowFullscreen());
|
||||
}
|
||||
@end
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
NSWindow *stream_window = nil;
|
||||
CrossDeskStreamFullscreenTarget *stream_fullscreen_target = nil;
|
||||
bool stream_fullscreen = false;
|
||||
NSRect stream_restore_frame = NSZeroRect;
|
||||
NSWindowStyleMask stream_restore_style_mask = NSWindowStyleMaskTitled;
|
||||
NSWindowTitleVisibility stream_restore_title_visibility =
|
||||
NSWindowTitleVisible;
|
||||
BOOL stream_restore_titlebar_transparent = NO;
|
||||
BOOL stream_restore_movable = YES;
|
||||
BOOL stream_restore_close_hidden = NO;
|
||||
BOOL stream_restore_miniaturize_hidden = NO;
|
||||
BOOL stream_restore_zoom_hidden = NO;
|
||||
NSApplicationPresentationOptions stream_restore_presentation_options =
|
||||
NSApplicationPresentationDefault;
|
||||
|
||||
void InstallStreamFullscreenButton(NSWindow *window) {
|
||||
if (window == nil) {
|
||||
return;
|
||||
}
|
||||
if (stream_fullscreen_target == nil) {
|
||||
stream_fullscreen_target =
|
||||
[[CrossDeskStreamFullscreenTarget alloc] init];
|
||||
}
|
||||
NSButton *zoom_button =
|
||||
[window standardWindowButton:NSWindowZoomButton];
|
||||
zoom_button.target = stream_fullscreen_target;
|
||||
zoom_button.action = @selector(toggleStreamFullscreen:);
|
||||
}
|
||||
|
||||
void SetTitlebarButtonsHidden(NSWindow *window, BOOL hidden) {
|
||||
[window standardWindowButton:NSWindowCloseButton].hidden = hidden;
|
||||
[window standardWindowButton:NSWindowMiniaturizeButton].hidden = hidden;
|
||||
[window standardWindowButton:NSWindowZoomButton].hidden = hidden;
|
||||
}
|
||||
|
||||
void CenterWindowOnScreen(NSWindow *window, NSScreen *screen) {
|
||||
if (window == nil || screen == nil) {
|
||||
return;
|
||||
}
|
||||
const NSRect available_frame = screen.visibleFrame;
|
||||
const NSRect window_frame = window.frame;
|
||||
const NSPoint centered_origin = NSMakePoint(
|
||||
NSMidX(available_frame) - NSWidth(window_frame) * 0.5,
|
||||
NSMidY(available_frame) - NSHeight(window_frame) * 0.5);
|
||||
[window setFrameOrigin:centered_origin];
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool StartNativeWindowDrag() {
|
||||
@autoreleasepool {
|
||||
NSEvent *event = [[NSApplication sharedApplication] currentEvent];
|
||||
if (event == nil || event.type != NSEventTypeLeftMouseDown ||
|
||||
event.window == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[event.window performWindowDragWithEvent:event];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool HideDisabledMainWindowZoomButton() {
|
||||
@autoreleasepool {
|
||||
for (NSWindow *window in [NSApp windows]) {
|
||||
if (![window.title isEqualToString:@"CrossDesk"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSButton *zoom_button =
|
||||
[window standardWindowButton:NSWindowZoomButton];
|
||||
// The main window has a fixed size, so AppKit disables its zoom button.
|
||||
// Stream windows remain resizable and must keep their native button.
|
||||
if (zoom_button == nil || zoom_button.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
zoom_button.hidden = YES;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigureStreamWindowLiveResize() {
|
||||
@autoreleasepool {
|
||||
for (NSWindow *window in [NSApp windows]) {
|
||||
if (![window.title isEqualToString:@"CrossDesk"]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSButton *zoom_button =
|
||||
[window standardWindowButton:NSWindowZoomButton];
|
||||
// The stream window is resizable and therefore keeps an enabled native
|
||||
// zoom button. The fixed-size main window's zoom button is disabled.
|
||||
if (zoom_button == nil || !zoom_button.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSView *content_view = window.contentView;
|
||||
if (content_view == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// During an ordinary edge resize, scale one cached frame and redraw once
|
||||
// the new window size has settled.
|
||||
window.preservesContentDuringLiveResize = YES;
|
||||
content_view.layerContentsRedrawPolicy =
|
||||
NSViewLayerContentsRedrawBeforeViewResize;
|
||||
content_view.layerContentsPlacement =
|
||||
NSViewLayerContentsPlacementScaleProportionallyToFit;
|
||||
stream_window = window;
|
||||
// Native Space fullscreen cross-fades an AppKit source snapshot over a
|
||||
// separately rendered destination window. Disable that path for the
|
||||
// stream window and route the green button to the immediate transition.
|
||||
window.collectionBehavior =
|
||||
(window.collectionBehavior &
|
||||
~(NSWindowCollectionBehaviorFullScreenPrimary |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary |
|
||||
NSWindowCollectionBehaviorFullScreenAllowsTiling)) |
|
||||
NSWindowCollectionBehaviorFullScreenNone;
|
||||
InstallStreamFullscreenButton(window);
|
||||
content_view.needsDisplay = YES;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SetStreamWindowFullscreen(bool fullscreen) {
|
||||
@autoreleasepool {
|
||||
if (stream_window == nil || stream_fullscreen == fullscreen) {
|
||||
return stream_window != nil;
|
||||
}
|
||||
|
||||
if (fullscreen) {
|
||||
NSScreen *screen = stream_window.screen ?: NSScreen.mainScreen;
|
||||
if (screen == nil) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream_restore_frame = stream_window.frame;
|
||||
stream_restore_style_mask = stream_window.styleMask;
|
||||
stream_restore_title_visibility = stream_window.titleVisibility;
|
||||
stream_restore_titlebar_transparent =
|
||||
stream_window.titlebarAppearsTransparent;
|
||||
stream_restore_movable = stream_window.movable;
|
||||
stream_restore_close_hidden =
|
||||
[stream_window standardWindowButton:NSWindowCloseButton].hidden;
|
||||
stream_restore_miniaturize_hidden =
|
||||
[stream_window standardWindowButton:NSWindowMiniaturizeButton].hidden;
|
||||
stream_restore_zoom_hidden =
|
||||
[stream_window standardWindowButton:NSWindowZoomButton].hidden;
|
||||
stream_restore_presentation_options = NSApp.presentationOptions;
|
||||
|
||||
stream_fullscreen = true;
|
||||
stream_window.styleMask =
|
||||
stream_restore_style_mask | NSWindowStyleMaskFullSizeContentView;
|
||||
stream_window.titleVisibility = NSWindowTitleHidden;
|
||||
stream_window.titlebarAppearsTransparent = YES;
|
||||
stream_window.movable = NO;
|
||||
SetTitlebarButtonsHidden(stream_window, YES);
|
||||
|
||||
NSApplicationPresentationOptions presentation_options =
|
||||
stream_restore_presentation_options;
|
||||
if ((presentation_options & (NSApplicationPresentationHideDock |
|
||||
NSApplicationPresentationAutoHideDock)) ==
|
||||
0) {
|
||||
presentation_options |= NSApplicationPresentationAutoHideDock;
|
||||
}
|
||||
if ((presentation_options & (NSApplicationPresentationHideMenuBar |
|
||||
NSApplicationPresentationAutoHideMenuBar)) ==
|
||||
0) {
|
||||
presentation_options |= NSApplicationPresentationAutoHideMenuBar;
|
||||
}
|
||||
NSApp.presentationOptions = presentation_options;
|
||||
[stream_window setFrame:screen.frame display:YES animate:NO];
|
||||
[stream_window makeKeyAndOrderFront:nil];
|
||||
} else {
|
||||
stream_fullscreen = false;
|
||||
NSApp.presentationOptions = stream_restore_presentation_options;
|
||||
stream_window.styleMask = stream_restore_style_mask;
|
||||
stream_window.titleVisibility = stream_restore_title_visibility;
|
||||
stream_window.titlebarAppearsTransparent =
|
||||
stream_restore_titlebar_transparent;
|
||||
stream_window.movable = stream_restore_movable;
|
||||
[stream_window setFrame:stream_restore_frame display:YES animate:NO];
|
||||
[stream_window standardWindowButton:NSWindowCloseButton].hidden =
|
||||
stream_restore_close_hidden;
|
||||
[stream_window standardWindowButton:NSWindowMiniaturizeButton].hidden =
|
||||
stream_restore_miniaturize_hidden;
|
||||
[stream_window standardWindowButton:NSWindowZoomButton].hidden =
|
||||
stream_restore_zoom_hidden;
|
||||
InstallStreamFullscreenButton(stream_window);
|
||||
[stream_window makeKeyAndOrderFront:nil];
|
||||
}
|
||||
|
||||
stream_window.contentView.alphaValue = 1.0;
|
||||
stream_window.contentView.needsDisplay = YES;
|
||||
[stream_window displayIfNeeded];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsStreamWindowFullscreen() {
|
||||
return stream_fullscreen;
|
||||
}
|
||||
|
||||
std::string OpenNativeFileDialog(const std::string &title) {
|
||||
@autoreleasepool {
|
||||
NSOpenPanel *panel = [NSOpenPanel openPanel];
|
||||
panel.canChooseFiles = YES;
|
||||
panel.canChooseDirectories = NO;
|
||||
panel.allowsMultipleSelection = NO;
|
||||
panel.resolvesAliases = YES;
|
||||
|
||||
NSString *prompt =
|
||||
[[NSString alloc] initWithBytes:title.data()
|
||||
length:title.size()
|
||||
encoding:NSUTF8StringEncoding];
|
||||
if (prompt != nil && prompt.length > 0) {
|
||||
panel.title = prompt;
|
||||
panel.message = prompt;
|
||||
}
|
||||
|
||||
// tinyfiledialogs uses an external osascript process on macOS, so its
|
||||
// chooser can appear without becoming the active window. Keep the picker
|
||||
// inside CrossDesk and explicitly activate both the app and panel.
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
NSWindow *owner = stream_window ?: NSApp.keyWindow ?: NSApp.mainWindow;
|
||||
if (owner != nil) {
|
||||
[owner makeKeyAndOrderFront:nil];
|
||||
}
|
||||
NSScreen *target_screen = owner.screen ?: NSScreen.mainScreen;
|
||||
CenterWindowOnScreen(panel, target_screen);
|
||||
[panel makeKeyAndOrderFront:nil];
|
||||
|
||||
if ([panel runModal] != NSModalResponseOK || panel.URL == nil) {
|
||||
return {};
|
||||
}
|
||||
const char *path = panel.URL.fileSystemRepresentation;
|
||||
return path != nullptr ? std::string(path) : std::string{};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -200,8 +200,6 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string &remote_id,
|
||||
}
|
||||
|
||||
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
|
||||
SDL_FlushEvent(STREAM_REFRESH_EVENT);
|
||||
|
||||
std::shared_ptr<std::vector<unsigned char>> frame_snapshot;
|
||||
int video_width = 0;
|
||||
int video_height = 0;
|
||||
@@ -290,11 +288,6 @@ void GuiRuntime::WaitForThumbnailSaveTasks() {
|
||||
|
||||
void GuiRuntime::ResetRemoteSessionResources(
|
||||
std::shared_ptr<RemoteSession> props) {
|
||||
if (props->stream_texture_) {
|
||||
SDL_DestroyTexture(props->stream_texture_);
|
||||
props->stream_texture_ = nullptr;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(props->video_frame_mutex_);
|
||||
props->front_frame_.reset();
|
||||
|
||||
@@ -168,10 +168,10 @@ void GuiRuntime::UpdateLabels() {
|
||||
|
||||
|
||||
void GuiRuntime::HandleRecentConnections() {
|
||||
if (reload_recent_connections_ && main_renderer_) {
|
||||
if (reload_recent_connections_ && thumbnail_) {
|
||||
uint32_t now_time = SDL_GetTicks();
|
||||
if (now_time - recent_connection_image_save_time_ >= 50) {
|
||||
int ret = thumbnail_->LoadThumbnail(main_renderer_, recent_connections_,
|
||||
int ret = thumbnail_->LoadThumbnail(recent_connections_,
|
||||
&recent_connection_image_width_,
|
||||
&recent_connection_image_height_);
|
||||
if (!ret) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "device_controller.h"
|
||||
#include "file_transfer.h"
|
||||
#include "localization.h"
|
||||
#include "filesystem_utf8.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
@@ -90,7 +91,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
std::string configured_path =
|
||||
runtime->config_center_->GetFileTransferSavePath();
|
||||
if (!configured_path.empty()) {
|
||||
receiver.SetOutputDir(std::filesystem::u8path(configured_path));
|
||||
receiver.SetOutputDir(PathFromUtf8(configured_path));
|
||||
} else if (receiver.OutputDir().empty()) {
|
||||
receiver = FileReceiver(); // re-init with default desktop path
|
||||
}
|
||||
|
||||
@@ -230,12 +230,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
runtime->need_to_create_stream_window_ = true;
|
||||
}
|
||||
props->connection_established_ = true;
|
||||
props->stream_render_rect_ = {
|
||||
0, (int)runtime->title_bar_height_, (int)runtime->stream_window_width_,
|
||||
(int)(runtime->stream_window_height_ - runtime->title_bar_height_)};
|
||||
props->stream_render_rect_f_ = {
|
||||
0.0f, runtime->title_bar_height_, runtime->stream_window_width_,
|
||||
runtime->stream_window_height_ - runtime->title_bar_height_};
|
||||
runtime->start_keyboard_capturer_ = true;
|
||||
break;
|
||||
}
|
||||
@@ -259,11 +253,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
props->stream_cleanup_pending_ = true;
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
event.type = runtime->STREAM_REFRESH_EVENT;
|
||||
event.user.data1 = props.get();
|
||||
SDL_PushEvent(&event);
|
||||
|
||||
runtime->focus_on_stream_window_ = false;
|
||||
|
||||
break;
|
||||
|
||||
@@ -70,12 +70,9 @@ void PeerEventHandler::OnReceiveVideoBuffer(
|
||||
props->video_size_ = video_frame->size;
|
||||
|
||||
props->front_frame_.swap(props->back_frame_);
|
||||
++props->video_frame_sequence_;
|
||||
}
|
||||
|
||||
SDL_Event event;
|
||||
event.type = runtime->STREAM_REFRESH_EVENT;
|
||||
event.user.data1 = props;
|
||||
SDL_PushEvent(&event);
|
||||
props->streaming_ = true;
|
||||
|
||||
if (props->net_traffic_stats_button_pressed_) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#ifndef CROSSDESK_GUI_REMOTE_SESSION_H_
|
||||
#define CROSSDESK_GUI_REMOTE_SESSION_H_
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
@@ -16,7 +14,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "display_info.h"
|
||||
#include "imgui.h"
|
||||
#include "minirtc.h"
|
||||
|
||||
namespace crossdesk::gui_detail {
|
||||
@@ -111,7 +108,7 @@ struct RemoteSession {
|
||||
double control_bar_button_pressed_time_ = 0;
|
||||
double net_traffic_stats_button_pressed_time_ = 0;
|
||||
|
||||
// Written by the decode callback thread and consumed by the SDL thread.
|
||||
// Written by the decode callback thread and consumed by the UI thread.
|
||||
std::mutex video_frame_mutex_;
|
||||
std::shared_ptr<std::vector<unsigned char>> front_frame_;
|
||||
std::shared_ptr<std::vector<unsigned char>> back_frame_;
|
||||
@@ -129,6 +126,7 @@ struct RemoteSession {
|
||||
int video_height_last_ = 0;
|
||||
int selected_display_ = 0;
|
||||
size_t video_size_ = 0;
|
||||
uint64_t video_frame_sequence_ = 0;
|
||||
bool tab_selected_ = false;
|
||||
bool tab_opened_ = true;
|
||||
std::optional<float> pos_x_before_docked_;
|
||||
@@ -146,15 +144,7 @@ struct RemoteSession {
|
||||
bool remote_service_available_ = false;
|
||||
std::string remote_interactive_stage_;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
SDL_Texture *stream_texture_ = nullptr;
|
||||
uint8_t *argb_buffer_ = nullptr;
|
||||
int argb_buffer_size_ = 0;
|
||||
SDL_FRect stream_render_rect_f_ = {0.0f, 0.0f, 0.0f, 0.0f};
|
||||
SDL_Rect stream_render_rect_{};
|
||||
SDL_Rect stream_render_rect_last_{};
|
||||
ImVec2 control_window_pos_{};
|
||||
|
||||
// Shared by minirtc callbacks, SDL rendering and SDL audio callbacks.
|
||||
// Shared by minirtc callbacks, Slint rendering and SDL audio callbacks.
|
||||
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
|
||||
TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
|
||||
int fps_ = 0;
|
||||
|
||||
@@ -91,6 +91,7 @@ struct PlatformIntegrationState {
|
||||
bool show_portable_service_prompt_suppressed_window_ = false;
|
||||
bool portable_service_do_not_remind_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
bool portable_service_installed_ = false;
|
||||
std::atomic<PortableServiceInstallState> portable_service_install_state_{
|
||||
PortableServiceInstallState::idle};
|
||||
std::thread portable_service_install_thread_;
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
// The legacy client uses ImGui::StyleColorsLight(). Keep the structural line
|
||||
// colors in one place so the Slint renderer produces the same visual weight.
|
||||
export global ImGuiLineStyle {
|
||||
out property <color> border: #0000004d;
|
||||
out property <color> separator: #6363639e;
|
||||
out property <color> divider: #0000007a;
|
||||
out property <color> control-separator: #b2b2b2;
|
||||
}
|
||||
|
||||
// ImGui loads a 32px font atlas and applies per-window font scales. These
|
||||
// logical sizes reproduce those visual tiers on Slint's HiDPI renderer.
|
||||
export global ImGuiFontStyle {
|
||||
out property <length> base: 12px;
|
||||
out property <length> section-title: 18px;
|
||||
out property <length> panel-label: 17px;
|
||||
out property <length> field-value: 22px;
|
||||
out property <length> prominent: 16px;
|
||||
out property <length> body: 11px;
|
||||
out property <length> small: 10px;
|
||||
}
|
||||
|
||||
// Font Awesome 6 Free Solid glyphs used by the legacy ImGui client. The font
|
||||
// data is registered for every Slint window by GuiApplication before show().
|
||||
export global FontAwesomeIcons {
|
||||
out property <string> angle-down: "\u{f107}";
|
||||
out property <string> angle-left: "\u{f104}";
|
||||
out property <string> angle-right: "\u{f105}";
|
||||
out property <string> angle-up: "\u{f106}";
|
||||
out property <string> arrow-right-long: "\u{f178}";
|
||||
out property <string> bars: "\u{f0c9}";
|
||||
out property <string> check: "\u{f00c}";
|
||||
out property <string> circle-arrow-up: "\u{f0aa}";
|
||||
out property <string> compress: "\u{f066}";
|
||||
out property <string> computer-mouse: "\u{f8cc}";
|
||||
out property <string> copy: "\u{f0c5}";
|
||||
out property <string> display: "\u{e163}";
|
||||
out property <string> expand: "\u{f065}";
|
||||
out property <string> eye: "\u{f06e}";
|
||||
out property <string> eye-slash: "\u{f070}";
|
||||
out property <string> folder-open: "\u{f07c}";
|
||||
out property <string> keyboard: "\u{f11c}";
|
||||
out property <string> minus: "\u{f068}";
|
||||
out property <string> pen: "\u{f304}";
|
||||
out property <string> signal: "\u{f012}";
|
||||
out property <string> shield-halved: "\u{f3ed}";
|
||||
out property <string> square-full: "\u{f45c}";
|
||||
out property <string> trash-can: "\u{f2ed}";
|
||||
out property <string> volume-high: "\u{f028}";
|
||||
out property <string> volume-xmark: "\u{f6a9}";
|
||||
out property <string> xmark: "\u{f00d}";
|
||||
}
|
||||
|
||||
export component IconButton inherits Rectangle {
|
||||
in property <string> icon;
|
||||
in property <string> tooltip;
|
||||
in property <bool> danger: false;
|
||||
in property <length> icon-size: 16px;
|
||||
in property <color> icon-color: #24272d;
|
||||
in property <color> normal-background: transparent;
|
||||
in property <color> hover-background: #edf0f5;
|
||||
in property <color> pressed-background: #d9dde5;
|
||||
in property <bool> slashed: false;
|
||||
callback clicked;
|
||||
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: touch.pressed ? (danger ? #d92d20 : root.pressed-background)
|
||||
: touch.has-hover ? (danger ? #f04438 : root.hover-background)
|
||||
: root.normal-background;
|
||||
border-radius: 4px;
|
||||
|
||||
Text {
|
||||
text: root.icon;
|
||||
color: touch.has-hover && root.danger ? white : root.icon-color;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: root.icon-size;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
if root.slashed: Text {
|
||||
text: "/";
|
||||
color: touch.has-hover && root.danger ? white : root.icon-color;
|
||||
font-size: root.icon-size + 7px;
|
||||
font-weight: 700;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
|
||||
if touch.has-hover && root.tooltip != "": Rectangle {
|
||||
x: min(root.width - self.width, 0px);
|
||||
y: root.height + 3px;
|
||||
width: tip.preferred-width + 12px;
|
||||
height: 24px;
|
||||
background: #22252be8;
|
||||
border-radius: 4px;
|
||||
z: 20;
|
||||
tip := Text {
|
||||
text: root.tooltip;
|
||||
color: white;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retained for non-font artwork. UI action icons use IconButton so the Slint
|
||||
// and ImGui clients share Font Awesome's glyph geometry.
|
||||
export component ImageButton inherits Rectangle {
|
||||
in property <image> icon;
|
||||
in property <string> tooltip;
|
||||
in property <bool> danger: false;
|
||||
in property <length> icon-width: 16px;
|
||||
in property <length> icon-height: 16px;
|
||||
callback clicked;
|
||||
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: touch.pressed ? (danger ? #d92d20 : #d9dde5)
|
||||
: touch.has-hover ? (danger ? #f04438 : #edf0f5)
|
||||
: transparent;
|
||||
border-radius: 4px;
|
||||
|
||||
Image {
|
||||
x: (parent.width - self.width) / 2;
|
||||
y: (parent.height - self.height) / 2;
|
||||
width: root.icon-width;
|
||||
height: root.icon-height;
|
||||
source: root.icon;
|
||||
image-fit: contain;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
|
||||
if touch.has-hover && root.tooltip != "": Rectangle {
|
||||
x: min(root.width - self.width, 0px);
|
||||
y: root.height + 3px;
|
||||
width: tip.preferred-width + 12px;
|
||||
height: 24px;
|
||||
background: #22252be8;
|
||||
border-radius: 4px;
|
||||
z: 20;
|
||||
tip := Text {
|
||||
text: root.tooltip;
|
||||
color: white;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component CompactComboBox inherits Rectangle {
|
||||
in property <[string]> model;
|
||||
in-out property <int> current-index: 0;
|
||||
in property <bool> enabled: true;
|
||||
property <bool> popup-animation-enabled: false;
|
||||
property <float> popup-progress: 0.0;
|
||||
callback selected(int);
|
||||
|
||||
animate popup-progress {
|
||||
duration: 160ms;
|
||||
easing: ease-out;
|
||||
enabled: root.popup-animation-enabled;
|
||||
}
|
||||
|
||||
width: 73px;
|
||||
height: 24px;
|
||||
background: root.enabled ? white : #f1f1f1;
|
||||
border-width: 1px;
|
||||
border-color: #bfc2c7;
|
||||
border-radius: 3px;
|
||||
|
||||
Text {
|
||||
x: 5px;
|
||||
width: parent.width - 24px;
|
||||
text: root.model[root.current-index];
|
||||
color: root.enabled ? #202124 : #8b8f95;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
overflow: elide;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Rectangle {
|
||||
x: parent.width - 21px;
|
||||
width: 20px;
|
||||
height: parent.height - 2px;
|
||||
y: 1px;
|
||||
background: root.enabled ? #9bc9f8 : #d7d9dc;
|
||||
Text {
|
||||
text: FontAwesomeIcons.angle-down;
|
||||
color: #202124;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 10px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
transform-rotation: combo-popup.is-open ? 180deg : 0deg;
|
||||
animate transform-rotation {
|
||||
duration: 140ms;
|
||||
easing: ease-out;
|
||||
enabled: combo-popup.is-open;
|
||||
}
|
||||
}
|
||||
}
|
||||
TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => {
|
||||
root.popup-animation-enabled = false;
|
||||
root.popup-progress = 0.0;
|
||||
combo-popup.show();
|
||||
popup-open-timer.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Start on the next frame so the native popup first renders at zero
|
||||
// height, making the downward expansion visible on every backend.
|
||||
popup-open-timer := Timer {
|
||||
interval: 16ms;
|
||||
running: false;
|
||||
triggered => {
|
||||
self.running = false;
|
||||
if combo-popup.is-open {
|
||||
root.popup-animation-enabled = true;
|
||||
root.popup-progress = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combo-popup := PopupWindow {
|
||||
x: 0px;
|
||||
y: root.height;
|
||||
width: root.width;
|
||||
height: root.model.length * 23px;
|
||||
close-policy: close-on-click-outside;
|
||||
|
||||
Rectangle {
|
||||
// Keep the reveal anchored to the popup's top edge while the
|
||||
// content settles downward into its final position.
|
||||
y: (1.0 - root.popup-progress) * -8px;
|
||||
width: parent.width;
|
||||
height: parent.height * root.popup-progress;
|
||||
opacity: root.popup-progress;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: #bfc2c7;
|
||||
border-radius: 3px;
|
||||
clip: true;
|
||||
for choice[index] in root.model: Rectangle {
|
||||
y: index * 23px;
|
||||
width: parent.width;
|
||||
height: 23px;
|
||||
background: option-touch.has-hover ? #dbeafb : index == root.current-index ? #eef5fd : white;
|
||||
Text { x: 5px; width: parent.width - 10px; text: choice; color: #202124; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
option-touch := TouchArea {
|
||||
clicked => {
|
||||
popup-open-timer.running = false;
|
||||
root.popup-animation-enabled = false;
|
||||
root.popup-progress = 0.0;
|
||||
root.current-index = index;
|
||||
root.selected(index);
|
||||
combo-popup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
background: transparent;
|
||||
border-width: 1px;
|
||||
border-color: #bfc2c7;
|
||||
border-radius: 3px;
|
||||
z: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component ImGuiCheckBox inherits Rectangle {
|
||||
in-out property <bool> checked;
|
||||
in property <bool> enabled: true;
|
||||
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
background: root.enabled ? white : #f1f1f1;
|
||||
border-width: 1px;
|
||||
border-color: check-touch.has-hover && root.enabled ? #8cbef2 : #b9bdc2;
|
||||
border-radius: 4px;
|
||||
if root.checked: Text {
|
||||
text: FontAwesomeIcons.check;
|
||||
color: #2e86de;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: 16px;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
check-touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.checked = !root.checked; }
|
||||
}
|
||||
}
|
||||
|
||||
export component MiniToggleSwitch inherits Rectangle {
|
||||
in-out property <bool> checked;
|
||||
in property <bool> enabled: true;
|
||||
callback toggled(bool);
|
||||
|
||||
width: 29px;
|
||||
height: 16px;
|
||||
background: !root.enabled ? (root.checked ? #6e9ddacc : #9a9a9acc)
|
||||
: root.checked ? #2463c7 : #9a9a9a;
|
||||
border-radius: 8px;
|
||||
Rectangle {
|
||||
x: root.checked ? parent.width - self.width - 2px : 2px;
|
||||
y: 2px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
}
|
||||
TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.checked = !root.checked; root.toggled(root.checked); }
|
||||
}
|
||||
}
|
||||
|
||||
export component CompactButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <bool> primary: false;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked;
|
||||
|
||||
width: 34px;
|
||||
height: 24px;
|
||||
background: !root.enabled ? #eceef0
|
||||
: button-touch.pressed ? (root.primary ? #82b8f2 : #d8dadd)
|
||||
: button-touch.has-hover ? (root.primary ? #b8d9fb : #f0f1f2)
|
||||
: root.primary ? #a8d0fa : #e2e4e7;
|
||||
border-width: 1px;
|
||||
border-color: root.primary ? #9bc3ed : #c8cbd0;
|
||||
border-radius: 3px;
|
||||
Text {
|
||||
text: root.text;
|
||||
color: root.enabled ? #24272d : #8e9298;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
overflow: elide;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
button-touch := TouchArea { enabled: root.enabled; clicked => { root.clicked(); } }
|
||||
}
|
||||
|
||||
export component PrimaryButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked;
|
||||
|
||||
min-width: 72px;
|
||||
height: 30px;
|
||||
background: !root.enabled ? #b8c1d1
|
||||
: touch.pressed ? #1849a9
|
||||
: touch.has-hover ? #2970d6
|
||||
: #2463c7;
|
||||
border-radius: 5px;
|
||||
|
||||
Text {
|
||||
text: root.text;
|
||||
color: white;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
export component SecondaryButton inherits Rectangle {
|
||||
in property <string> text;
|
||||
in property <bool> enabled: true;
|
||||
callback clicked;
|
||||
|
||||
min-width: 72px;
|
||||
height: 30px;
|
||||
background: !root.enabled ? #f1f2f4
|
||||
: touch.pressed ? #dfe3e9
|
||||
: touch.has-hover ? #f3f4f6 : white;
|
||||
border-width: 1px;
|
||||
border-color: #c9ced7;
|
||||
border-radius: 5px;
|
||||
clip: true;
|
||||
|
||||
Text {
|
||||
text: root.text;
|
||||
color: root.enabled ? #24272d : #949aa5;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
export component ToggleSwitch inherits Rectangle {
|
||||
in-out property <bool> checked;
|
||||
in property <bool> enabled: true;
|
||||
callback toggled(bool);
|
||||
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
background: !root.enabled ? #d0d5dd : root.checked ? #2463c7 : #98a2b3;
|
||||
border-radius: self.height / 2;
|
||||
|
||||
Rectangle {
|
||||
x: root.checked ? parent.width - self.width - 3px : 3px;
|
||||
y: 3px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 7px;
|
||||
background: white;
|
||||
animate x { duration: 100ms; easing: ease-in-out; }
|
||||
}
|
||||
|
||||
TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => {
|
||||
root.checked = !root.checked;
|
||||
root.toggled(root.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component SectionTitle inherits Text {
|
||||
color: #00000080;
|
||||
font-size: ImGuiFontStyle.section-title;
|
||||
font-weight: 500;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
export component DialogSurface inherits Rectangle {
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Single AOT entry point. Generating one header for the entire UI avoids
|
||||
// duplicate imported-component definitions when C++ uses all three windows.
|
||||
export { MainWindow, RecentConnection, UiStrings } from "main_window.slint";
|
||||
export { StreamWindow, StreamTab, FileTransferEntry, NetworkStatsRow, StreamStrings } from "stream_window.slint";
|
||||
export { ServerWindow, ControllerEntry } from "server_window.slint";
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M2 9h13m-5-5 5 5-5 5" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 189 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 22"><path d="M3 11h25M20 3l8 8-8 8" fill="none" stroke="#050505" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 195 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m2 8 3.2 3.2L14 2.8" fill="none" stroke="#2389e8" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 193 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 8"><path d="M1 2l4 4 4-4" fill="none" stroke="#111" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 182 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 14"><path d="m8 1-6 6 6 6" fill="none" stroke="#202124" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 186 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 14"><path d="m2 1 6 6-6 6" fill="none" stroke="#202124" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 186 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 9"><path d="m1 7 5-5 5 5" fill="none" stroke="#202124" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
|
After Width: | Height: | Size: 185 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3 3l10 10M13 3L3 13" fill="none" stroke="#fff" stroke-width="1.7"/></svg>
|
||||
|
After Width: | Height: | Size: 144 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3 3l10 10M13 3L3 13" fill="none" stroke="#000000" stroke-width="1"/></svg>
|
||||
|
After Width: | Height: | Size: 145 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M7 7H2m5 0V2m4 5h5m-5 0V2M7 11H2m5 0v5m4-5h5m-5 0v5" fill="none" stroke="#202124" stroke-width="1.5"/></svg>
|
||||
|
After Width: | Height: | Size: 178 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><rect x="6" y="3" width="9" height="11" rx="1.2" fill="#111"/><rect x="3" y="6" width="9" height="10" rx="1.2" fill="#111" stroke="#eff0f2" stroke-width="1.4"/></svg>
|
||||
|
After Width: | Height: | Size: 227 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M7 2H2v5M11 2h5v5M7 16H2v-5m9 5h5v-5" fill="none" stroke="#202124" stroke-width="1.5"/><path d="M2 2l5 5m9-5-5 5M2 16l5-5m9 5-5-5" stroke="#202124" stroke-width="1.3"/></svg>
|
||||
|
After Width: | Height: | Size: 244 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M1.5 9s2.7-4.5 7.5-4.5S16.5 9 16.5 9 13.8 13.5 9 13.5 1.5 9 1.5 9Z" fill="none" stroke="#111" stroke-width="1.6"/><path d="M2 2l14 14" stroke="#111" stroke-width="2"/></svg>
|
||||
|
After Width: | Height: | Size: 243 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M1.5 9s2.7-4.5 7.5-4.5S16.5 9 16.5 9 13.8 13.5 9 13.5 1.5 9 1.5 9Z" fill="none" stroke="#111" stroke-width="1.8"/><circle cx="9" cy="9" r="2.4" fill="#111"/></svg>
|
||||
|
After Width: | Height: | Size: 233 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M4 1h7l3 3v13H4V1Z" fill="none" stroke="#202124" stroke-width="1.4"/><path d="M11 1v4h4M9 14V8m-3 3 3-3 3 3" fill="none" stroke="#202124" stroke-width="1.4"/></svg>
|
||||
|
After Width: | Height: | Size: 234 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 16"><rect x="1" y="3" width="16" height="11" rx="1.5" fill="none" stroke="#202124" stroke-width="1.4"/><path d="M4 6h1m2 0h1m2 0h1m2 0h1M4 9h1m2 0h1m2 0h1m2 0h1M5 12h8" stroke="#202124" stroke-width="1.2" stroke-linecap="round"/></svg>
|
||||
|
After Width: | Height: | Size: 292 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect x="3" y="3" width="10" height="10" fill="none" stroke="#000000" stroke-width="1"/></svg>
|
||||
|
After Width: | Height: | Size: 155 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M2 4h12M2 8h12M2 12h12" fill="none" stroke="#000000" stroke-width="1" stroke-linecap="square"/></svg>
|
||||
|
After Width: | Height: | Size: 171 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M2 8h12" fill="none" stroke="#000000" stroke-width="1"/></svg>
|
||||
|
After Width: | Height: | Size: 132 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><rect x="2" y="3" width="14" height="10" rx="1" fill="none" stroke="#202124" stroke-width="1.5"/><path d="M6 16h6M9 13v3" stroke="#202124" stroke-width="1.5"/></svg>
|
||||
|
After Width: | Height: | Size: 226 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><rect x="4" y="1" width="10" height="16" rx="5" fill="none" stroke="#202124" stroke-width="1.4"/><path d="M2 2l14 14" stroke="#202124" stroke-width="1.7"/></svg>
|
||||
|
After Width: | Height: | Size: 222 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 18"><rect x="3" y="1" width="10" height="16" rx="5" fill="none" stroke="#202124" stroke-width="1.5"/><path d="M8 2v5" stroke="#202124" stroke-width="1.5"/></svg>
|
||||
|
After Width: | Height: | Size: 218 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M3 12.8 11.9 4l2.2 2.2-8.8 8.9-3.1.7.8-3Zm9.7-9.6 1-1a1.4 1.4 0 0 1 2 0l.2.2a1.4 1.4 0 0 1 0 2l-1 1-2.2-2.2Z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 199 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M3 12.8 11.9 4l2.2 2.2-8.8 8.9-3.1.7.8-3Z" fill="#111"/><path d="m12.7 3.2 1-1a1.4 1.4 0 0 1 2 0l.2.2a1.4 1.4 0 0 1 0 2l-1 1-2.2-2.2Z" fill="#111"/></svg>
|
||||
|
After Width: | Height: | Size: 224 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M2 16V9h3v7H2Zm5 0V5h3v11H7Zm5 0V2h3v14h-3Z" fill="#202124"/></svg>
|
||||
|
After Width: | Height: | Size: 137 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M5 5h8l-.6 10H5.6L5 5Zm2-2h4l1 1H6l1-1ZM4 4h10v1H4V4Z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 144 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M5 5h8l-.6 10H5.6L5 5Zm2-2h4l1 1H6l1-1ZM4 4h10v1H4V4Z" fill="#111"/></svg>
|
||||
|
After Width: | Height: | Size: 144 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M2 7h3l4-4v12l-4-4H2V7Z" fill="#202124"/><path d="M12 6l4 6m0-6-4 6" stroke="#202124" stroke-width="1.5"/></svg>
|
||||
|
After Width: | Height: | Size: 182 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><path d="M2 7h3l4-4v12l-4-4H2V7Z" fill="#202124"/><path d="M12 6a4 4 0 0 1 0 6m2-8a7 7 0 0 1 0 10" fill="none" stroke="#202124" stroke-width="1.4" stroke-linecap="round"/></svg>
|
||||
|
After Width: | Height: | Size: 238 B |
@@ -0,0 +1,149 @@
|
||||
import { CompactComboBox, FontAwesomeIcons, IconButton, SecondaryButton, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
|
||||
|
||||
export struct ControllerEntry {
|
||||
remote-id: string,
|
||||
display-name: string,
|
||||
}
|
||||
|
||||
export component ServerWindow inherits Window {
|
||||
title: "CrossDesk";
|
||||
preferred-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
|
||||
preferred-height: 150px;
|
||||
min-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
|
||||
max-width: root.language-index == 0 ? 250px : root.language-index == 1 ? 330px : 430px;
|
||||
no-frame: true;
|
||||
always-on-top: true;
|
||||
background: transparent;
|
||||
default-font-size: ImGuiFontStyle.base;
|
||||
|
||||
in property <[ControllerEntry]> controllers;
|
||||
in property <[string]> controller-names;
|
||||
in property <int> language-index: 0;
|
||||
in-out property <int> selected-controller: 0;
|
||||
in property <string> connection-label: "Connection status";
|
||||
in property <string> connection-status: "";
|
||||
in property <string> controller-label: "Controller";
|
||||
in property <string> file-transfer-label: "File transfer";
|
||||
in property <string> select-file-label: "Select file";
|
||||
in property <bool> sending-file: false;
|
||||
in property <bool> file-transfer-visible: false;
|
||||
in property <float> file-progress: 0;
|
||||
in property <string> file-progress-text: "";
|
||||
in property <string> current-file-name: "";
|
||||
in property <string> file-size-text: "";
|
||||
|
||||
callback title-drag(int, length, length);
|
||||
callback toggle-collapsed(bool);
|
||||
callback controller-selected(int);
|
||||
callback select-file;
|
||||
callback disconnect-controller;
|
||||
|
||||
private property <bool> collapsed: false;
|
||||
private property <bool> transfer-hovered: false;
|
||||
|
||||
Rectangle {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 8px;
|
||||
clip: true;
|
||||
|
||||
Rectangle {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
width: parent.width;
|
||||
height: 30px;
|
||||
background: #f8f9fb;
|
||||
Rectangle { y: parent.height - 1px; height: 1px; background: ImGuiLineStyle.border; }
|
||||
IconButton {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
width: 30px; height: 30px;
|
||||
icon: root.collapsed ? FontAwesomeIcons.angle-down : FontAwesomeIcons.angle-up;
|
||||
icon-size: 12px;
|
||||
clicked => { root.collapsed = !root.collapsed; root.toggle-collapsed(root.collapsed); }
|
||||
}
|
||||
drag := TouchArea {
|
||||
x: 30px; width: parent.width - 30px;
|
||||
property <int> phase: self.pressed ? 1 : 0;
|
||||
changed phase => { root.title-drag(phase, self.mouse-x, self.mouse-y); }
|
||||
moved => { if (self.pressed) { root.title-drag(2, self.mouse-x, self.mouse-y); } }
|
||||
}
|
||||
}
|
||||
|
||||
if !root.collapsed: Rectangle {
|
||||
x: 12px; y: 38px; width: parent.width - 52px; height: 101px;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px;
|
||||
VerticalLayout {
|
||||
padding: 8px; spacing: 3px; alignment: start;
|
||||
HorizontalLayout {
|
||||
height: 24px; spacing: 5px;
|
||||
Text { width: root.language-index == 0 ? 69px : 110px; text: root.controller-label; color: #434953; font-size: ImGuiFontStyle.body; vertical-alignment: center; overflow: elide; }
|
||||
CompactComboBox {
|
||||
width: root.language-index == 0 ? 108px : root.language-index == 1 ? 147px : 247px;
|
||||
model: root.controller-names;
|
||||
current-index <=> root.selected-controller;
|
||||
selected(index) => { root.controller-selected(index); }
|
||||
}
|
||||
}
|
||||
Rectangle { height: 1px; background: ImGuiLineStyle.separator; }
|
||||
HorizontalLayout {
|
||||
height: 24px; spacing: 5px;
|
||||
Text { width: root.language-index == 0 ? 69px : 110px; text: root.connection-label; color: #434953; font-size: ImGuiFontStyle.body; vertical-alignment: center; overflow: elide; }
|
||||
Text { text: root.connection-status; color: #2463c7; font-size: ImGuiFontStyle.body; horizontal-alignment: left; vertical-alignment: center; overflow: elide; }
|
||||
}
|
||||
Rectangle { height: 1px; background: ImGuiLineStyle.separator; }
|
||||
Rectangle {
|
||||
height: 24px;
|
||||
HorizontalLayout {
|
||||
y: 0px;
|
||||
width: parent.width;
|
||||
height: 24px;
|
||||
spacing: 5px;
|
||||
Text { width: root.language-index == 0 ? 69px : 110px; text: root.file-transfer-label; color: #434953; font-size: ImGuiFontStyle.body; vertical-alignment: center; overflow: elide; }
|
||||
SecondaryButton { width: root.language-index == 0 ? 78px : root.language-index == 1 ? 140px : 205px; height: 24px; text: root.select-file-label; clicked => { root.select-file(); } }
|
||||
Rectangle {
|
||||
visible: root.file-transfer-visible;
|
||||
width: 35px;
|
||||
Text {
|
||||
text: root.sending-file ? "↑ " + round(root.file-progress * 100) + "%" : "✓";
|
||||
color: root.sending-file ? #2463c7 : #25874b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
transfer-touch := TouchArea {
|
||||
changed has-hover => { root.transfer-hovered = self.has-hover; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !root.collapsed: IconButton {
|
||||
x: parent.width - 34px; y: 38px; width: 24px; height: 101px;
|
||||
icon: FontAwesomeIcons.xmark;
|
||||
icon-size: 13px;
|
||||
icon-color: white;
|
||||
normal-background: #ef2b2d;
|
||||
hover-background: #d92d20;
|
||||
pressed-background: #b42318;
|
||||
border-radius: 5px;
|
||||
clicked => { root.disconnect-controller(); }
|
||||
}
|
||||
if root.file-transfer-visible && root.transfer-hovered: Rectangle {
|
||||
x: 15px; y: 67px; width: root.width - 30px; height: 70px;
|
||||
background: #22252bf2;
|
||||
border-radius: 5px;
|
||||
z: 20;
|
||||
VerticalLayout {
|
||||
padding: 8px; spacing: 3px;
|
||||
Text { text: root.current-file-name; color: white; font-size: ImGuiFontStyle.small; overflow: elide; }
|
||||
Text { text: root.file-size-text; color: #e1e4e8; font-size: ImGuiFontStyle.small; }
|
||||
Text { text: round(root.file-progress * 100) + "% " + root.file-progress-text; color: #e1e4e8; font-size: ImGuiFontStyle.small; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
import { ComboBox, ScrollView } from "std-widgets.slint";
|
||||
import { FontAwesomeIcons, IconButton, PrimaryButton, ImGuiLineStyle, ImGuiFontStyle } from "common.slint";
|
||||
|
||||
export struct StreamTab {
|
||||
remote-id: string,
|
||||
title: string,
|
||||
connected: bool,
|
||||
}
|
||||
|
||||
export struct FileTransferEntry {
|
||||
name: string,
|
||||
status: string,
|
||||
progress: float,
|
||||
speed: string,
|
||||
size: string,
|
||||
}
|
||||
|
||||
export struct NetworkStatsRow {
|
||||
label: string,
|
||||
inbound: string,
|
||||
outbound: string,
|
||||
loss-rate: string,
|
||||
}
|
||||
|
||||
export global StreamStrings {
|
||||
in-out property <string> select-display: "Select Display";
|
||||
in-out property <string> send-shortcut: "Send Shortcut";
|
||||
in-out property <string> control-mouse: "Control Mouse";
|
||||
in-out property <string> release-mouse: "Release Mouse";
|
||||
in-out property <string> audio: "Audio Capture";
|
||||
in-out property <string> mute: "Mute";
|
||||
in-out property <string> select-file: "Select File to Send";
|
||||
in-out property <string> show-stats: "Show Net Traffic Stats";
|
||||
in-out property <string> hide-stats: "Hide Net Traffic Stats";
|
||||
in-out property <string> fullscreen: "Fullscreen";
|
||||
in-out property <string> exit-fullscreen: "Exit fullscreen";
|
||||
in-out property <string> disconnect: "Disconnect";
|
||||
in-out property <string> file-transfer: "File Transfer Progress";
|
||||
in-out property <string> expand-control: "Expand Control Bar";
|
||||
in-out property <string> collapse-control: "Collapse Control Bar";
|
||||
in-out property <string> stats-in: "In";
|
||||
in-out property <string> stats-out: "Out";
|
||||
in-out property <string> stats-loss-rate: "Loss Rate";
|
||||
in-out property <string> stats-resolution: "Res";
|
||||
in-out property <string> stats-connection-mode: "Mode";
|
||||
}
|
||||
|
||||
// The legacy ImGui control bar uses 24px buttons with Font Awesome rendered
|
||||
// at half of the 32px atlas size. Keep this local to the stream window: the
|
||||
// shared IconButton is intentionally larger and is used by the other windows.
|
||||
component ControlBarButton inherits Rectangle {
|
||||
in property <string> icon;
|
||||
in property <string> badge: "";
|
||||
in property <string> tooltip: "";
|
||||
in property <bool> selected: false;
|
||||
in property <bool> slashed: false;
|
||||
in property <length> icon-size: 16px;
|
||||
callback clicked;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: touch.pressed ? #0f87fa
|
||||
: touch.has-hover ? #4296fa
|
||||
: root.selected ? #4296fa : #4296fa66;
|
||||
border-radius: 3px;
|
||||
|
||||
Text {
|
||||
text: root.icon;
|
||||
color: #24272d;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: root.icon-size;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
// Draw the selected display number over the center of the monitor glyph.
|
||||
if root.badge != "": Text {
|
||||
// Use the rendered badge size so one- and multi-digit IDs share the
|
||||
// exact horizontal and vertical center of the button.
|
||||
x: (parent.width - self.preferred-width) / 2 + 0.5px;
|
||||
y: (parent.height - self.preferred-height) / 2 - 0.5px;
|
||||
text: root.badge;
|
||||
color: black;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
// Match the two offset diagonal strokes used by the ImGui mouse/audio
|
||||
// disabled state instead of covering the glyph with a full-width slash.
|
||||
if root.slashed: Rectangle {
|
||||
x: 11px;
|
||||
y: 0px;
|
||||
width: 2px;
|
||||
height: 24px;
|
||||
background: black;
|
||||
transform-rotation: -45deg;
|
||||
}
|
||||
if root.slashed: Rectangle {
|
||||
x: 9.5px;
|
||||
y: 1.5px;
|
||||
width: 2px;
|
||||
height: 24px;
|
||||
background: touch.has-hover ? #4296fa : #b3d5fd;
|
||||
transform-rotation: -45deg;
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
export component StreamWindow inherits Window {
|
||||
title: "CrossDesk";
|
||||
preferred-width: 1280px;
|
||||
preferred-height: 720px;
|
||||
min-width: 640px;
|
||||
min-height: 360px;
|
||||
no-frame: false;
|
||||
background: black;
|
||||
default-font-size: ImGuiFontStyle.base;
|
||||
|
||||
in property <[StreamTab]> tabs;
|
||||
in-out property <int> selected-tab: 0;
|
||||
in property <image> frame;
|
||||
in property <bool> has-frame: false;
|
||||
in property <string> status-text: "";
|
||||
in property <string> receiving-text: "";
|
||||
in property <[string]> displays;
|
||||
in-out property <int> selected-display: 0;
|
||||
in property <bool> mouse-control-enabled: true;
|
||||
in property <bool> audio-enabled: true;
|
||||
in property <bool> srtp-enabled: false;
|
||||
in property <bool> fullscreen-enabled: false;
|
||||
in property <bool> stats-visible: false;
|
||||
in property <[NetworkStatsRow]> stats-rows;
|
||||
in property <string> stats-fps: "0";
|
||||
in property <string> stats-resolution: "";
|
||||
in property <string> stats-connection-mode: "";
|
||||
in property <bool> file-transfer-visible: false;
|
||||
in property <[FileTransferEntry]> file-transfers;
|
||||
|
||||
callback select-tab(int);
|
||||
callback reorder-tab(int, length, length);
|
||||
callback close-tab(string);
|
||||
callback switch-display(int);
|
||||
callback send-shortcut(string);
|
||||
callback toggle-mouse-control;
|
||||
callback toggle-audio;
|
||||
callback select-file;
|
||||
callback close-file-transfer;
|
||||
pure callback can-drop-file(data-transfer) -> bool;
|
||||
callback file-dropped(data-transfer);
|
||||
callback toggle-stats;
|
||||
callback toggle-fullscreen;
|
||||
callback disconnect;
|
||||
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
|
||||
callback scroll-input(length, length, length, length);
|
||||
callback key-input(string, bool, bool, bool, bool, bool);
|
||||
|
||||
private property <bool> control-expanded: true;
|
||||
private property <bool> display-menu-open: false;
|
||||
private property <bool> shortcut-menu-open: false;
|
||||
private property <bool> control-docked-left: true;
|
||||
private property <bool> control-dragging: false;
|
||||
private property <length> control-drag-x: 0px;
|
||||
// Start flush against the title/tab strip. This remains mutable so a
|
||||
// manually dragged control bar keeps its chosen vertical position.
|
||||
private property <length> control-pos-y: 0px;
|
||||
private property <length> control-press-x: 0px;
|
||||
private property <length> control-press-y: 0px;
|
||||
private property <int> dragging-tab: -1;
|
||||
callback begin-control-drag(length, length);
|
||||
callback move-control-drag(length, length);
|
||||
callback end-control-drag;
|
||||
|
||||
begin-control-drag(press-x, press-y) => {
|
||||
// Capture the snapped position before switching x to control-drag-x.
|
||||
// Otherwise a second press briefly restores the previous drag target
|
||||
// and makes a plain click move the control bar.
|
||||
let current-x = control.x;
|
||||
let current-y = control.y;
|
||||
root.control-drag-x = current-x;
|
||||
// Store the pointer offset in the control bar, while all following
|
||||
// pointer positions remain relative to the stationary video surface.
|
||||
root.control-press-x = press-x - current-x;
|
||||
root.control-press-y = press-y - current-y;
|
||||
root.display-menu-open = false;
|
||||
root.shortcut-menu-open = false;
|
||||
root.control-dragging = true;
|
||||
}
|
||||
move-control-drag(pointer-x, pointer-y) => {
|
||||
if root.control-dragging {
|
||||
root.control-drag-x = min(max(0px, pointer-x - root.control-press-x), video.width - control.width);
|
||||
root.control-pos-y = min(max(0px, pointer-y - root.control-press-y), video.height - control.height);
|
||||
}
|
||||
}
|
||||
end-control-drag() => {
|
||||
if root.control-dragging {
|
||||
root.control-docked-left = root.control-drag-x + control.width / 2 < video.width / 2;
|
||||
root.control-pos-y = min(max(0px, control.y), video.height - control.height);
|
||||
root.control-dragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
background: black;
|
||||
|
||||
// Keep the ImGui tab strip behavior, including pointer-drag reordering.
|
||||
Rectangle {
|
||||
y: 0px;
|
||||
height: root.tabs.length > 1 ? 30px : 0px;
|
||||
visible: !root.fullscreen-enabled && root.tabs.length > 1;
|
||||
background: #f1f2f4;
|
||||
z: 10;
|
||||
HorizontalLayout {
|
||||
spacing: 1px;
|
||||
for tab[index] in root.tabs: tab-item := Rectangle {
|
||||
width: min(190px, max(110px, (parent.width - 12px) / root.tabs.length));
|
||||
background: index == root.selected-tab ? white : tab-touch.has-hover || root.dragging-tab == index ? #e5e7eb : #d8dbe0;
|
||||
border-width: root.dragging-tab == index ? 1px : 0px;
|
||||
border-color: #2463c7;
|
||||
Rectangle {
|
||||
width: parent.width - 27px;
|
||||
height: parent.height;
|
||||
background: transparent;
|
||||
if root.srtp-enabled: Text {
|
||||
x: 0px;
|
||||
width: 12px;
|
||||
height: parent.height;
|
||||
text: FontAwesomeIcons.shield-halved;
|
||||
color: #30343b;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: ImGuiFontStyle.base;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
x: root.srtp-enabled ? 16px : 0px;
|
||||
width: parent.width - self.x;
|
||||
height: parent.height;
|
||||
text: tab.title;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.base;
|
||||
overflow: elide;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
y: parent.height - 2px;
|
||||
height: 2px;
|
||||
background: index == root.selected-tab ? #3b82f6 : transparent;
|
||||
}
|
||||
tab-touch := TouchArea {
|
||||
width: parent.width - 27px;
|
||||
changed pressed => {
|
||||
if (self.pressed) {
|
||||
root.dragging-tab = index;
|
||||
} else if (root.dragging-tab == index) {
|
||||
root.reorder-tab(index, tab-item.x + self.mouse-x, tab-item.width);
|
||||
root.dragging-tab = -1;
|
||||
}
|
||||
}
|
||||
clicked => { root.selected-tab = index; root.select-tab(index); }
|
||||
}
|
||||
IconButton {
|
||||
x: parent.width - 27px;
|
||||
y: 1px;
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
z: 2;
|
||||
icon: FontAwesomeIcons.xmark;
|
||||
icon-size: 11px;
|
||||
icon-color: #555c66;
|
||||
danger: true;
|
||||
clicked => { root.close-tab(tab.remote-id); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
video := Rectangle {
|
||||
y: root.fullscreen-enabled ? 0px : (root.tabs.length > 1 ? 30px : 0px);
|
||||
height: parent.height - self.y;
|
||||
background: black;
|
||||
clip: true;
|
||||
|
||||
frame-image := Image {
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
source: root.frame;
|
||||
image-fit: contain;
|
||||
visible: root.has-frame;
|
||||
}
|
||||
|
||||
Text {
|
||||
// A connection state and the frame-waiting state may change in
|
||||
// the same UI tick. Render them through one text item so two
|
||||
// independent labels can never occupy the center together.
|
||||
visible: !root.has-frame && (root.receiving-text != "" || root.status-text != "");
|
||||
text: root.receiving-text != "" ? root.receiving-text : root.status-text;
|
||||
color: root.receiving-text != "" ? #ffffffea : white;
|
||||
font-size: root.receiving-text != "" ? 14px : ImGuiFontStyle.prominent;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
drop-zone := DropArea {
|
||||
can-drop(event) => {
|
||||
return root.can-drop-file(event.data) ? DragAction.copy : DragAction.none;
|
||||
}
|
||||
dropped(event) => {
|
||||
root.file-dropped(event.data);
|
||||
return DragAction.copy;
|
||||
}
|
||||
}
|
||||
|
||||
input-focus := FocusScope {
|
||||
focus-on-click: true;
|
||||
key-pressed(event) => {
|
||||
root.key-input(event.text, true, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||
accept
|
||||
}
|
||||
key-released(event) => {
|
||||
root.key-input(event.text, false, event.modifiers.control, event.modifiers.alt, event.modifiers.shift, event.modifiers.meta);
|
||||
accept
|
||||
}
|
||||
pointer := TouchArea {
|
||||
pointer-event(event) => {
|
||||
let over-control = self.mouse-x >= control.x
|
||||
&& self.mouse-x <= control.x + control.width
|
||||
&& self.mouse-y >= control.y
|
||||
&& self.mouse-y <= control.y + control.height;
|
||||
if event.kind == PointerEventKind.down
|
||||
&& event.button == PointerEventButton.left
|
||||
&& over-control {
|
||||
root.begin-control-drag(self.mouse-x, self.mouse-y);
|
||||
} else if root.control-dragging
|
||||
&& (event.kind == PointerEventKind.up
|
||||
|| event.kind == PointerEventKind.cancel) {
|
||||
root.end-control-drag();
|
||||
} else if !root.control-dragging {
|
||||
root.pointer-input(event.button, event.kind, self.mouse-x, self.mouse-y);
|
||||
}
|
||||
}
|
||||
moved => {
|
||||
if root.control-dragging {
|
||||
root.move-control-drag(self.mouse-x, self.mouse-y);
|
||||
}
|
||||
}
|
||||
scroll-event(event) => {
|
||||
if self.mouse-x < control.x
|
||||
|| self.mouse-x > control.x + control.width
|
||||
|| self.mouse-y < control.y
|
||||
|| self.mouse-y > control.y + control.height {
|
||||
root.scroll-input(event.delta-x, event.delta-y, self.mouse-x, self.mouse-y);
|
||||
}
|
||||
accept
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Floating control bar, matching the original top-left overlay.
|
||||
control := Rectangle {
|
||||
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
|
||||
y: min(max(0px, root.control-pos-y), parent.height - self.height);
|
||||
width: root.control-expanded ? min(300px, parent.width) : 20px;
|
||||
height: root.stats-visible && root.control-expanded ? min(180px, parent.height) : 38px;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 6px;
|
||||
z: 8;
|
||||
clip: true;
|
||||
// Keep width changes atomic. When docked right, animating both
|
||||
// x and the clip width can leave newly revealed buttons outside
|
||||
// Slint's damage region until they receive a hover repaint.
|
||||
animate height { duration: 60ms; easing: ease-out; }
|
||||
|
||||
if root.control-expanded && !root.control-docked-left: Rectangle {
|
||||
x: 31px; y: 12px; width: 2px; height: 15px;
|
||||
background: ImGuiLineStyle.control-separator;
|
||||
}
|
||||
|
||||
if root.control-expanded: display-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 9px : 42px; y: 7px;
|
||||
icon: FontAwesomeIcons.display;
|
||||
badge: root.selected-display + 1;
|
||||
tooltip: StreamStrings.select-display;
|
||||
clicked => { root.display-menu-open = !root.display-menu-open; root.shortcut-menu-open = false; }
|
||||
}
|
||||
if root.control-expanded: shortcut-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 41px : 74px; y: 7px;
|
||||
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
|
||||
clicked => { root.shortcut-menu-open = !root.shortcut-menu-open; root.display-menu-open = false; }
|
||||
}
|
||||
if root.control-expanded: mouse-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 73px : 106px; y: 7px;
|
||||
icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled;
|
||||
tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse;
|
||||
clicked => { root.toggle-mouse-control(); }
|
||||
}
|
||||
if root.control-expanded: audio-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 105px : 138px; y: 7px;
|
||||
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
|
||||
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
|
||||
clicked => { root.toggle-audio(); }
|
||||
}
|
||||
if root.control-expanded: file-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 137px : 170px; y: 7px;
|
||||
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
|
||||
clicked => { root.select-file(); }
|
||||
}
|
||||
if root.control-expanded: stats-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 169px : 202px; y: 7px;
|
||||
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
|
||||
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
|
||||
clicked => { root.toggle-stats(); }
|
||||
}
|
||||
if root.control-expanded: fullscreen-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 201px : 234px; y: 7px;
|
||||
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
|
||||
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
|
||||
clicked => { root.toggle-fullscreen(); }
|
||||
}
|
||||
if root.control-expanded: close-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 233px : 266px; y: 7px;
|
||||
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
|
||||
clicked => { root.disconnect(); }
|
||||
}
|
||||
|
||||
if root.control-expanded && root.control-docked-left: Rectangle {
|
||||
x: 267px; y: 12px; width: 2px; height: 15px;
|
||||
background: ImGuiLineStyle.control-separator;
|
||||
}
|
||||
collapse-button := ControlBarButton {
|
||||
x: root.control-expanded ? (root.control-docked-left ? 275px : 10px) : 2px;
|
||||
y: 7px;
|
||||
width: 15px;
|
||||
icon: root.control-expanded
|
||||
? (root.control-docked-left ? FontAwesomeIcons.angle-left : FontAwesomeIcons.angle-right)
|
||||
: (root.control-docked-left ? FontAwesomeIcons.angle-right : FontAwesomeIcons.angle-left);
|
||||
tooltip: root.control-expanded ? StreamStrings.collapse-control : StreamStrings.expand-control;
|
||||
clicked => { root.control-expanded = !root.control-expanded; }
|
||||
}
|
||||
|
||||
if root.stats-visible && root.control-expanded: stats-table := Rectangle {
|
||||
x: 14px;
|
||||
y: 38px;
|
||||
width: parent.width - 28px;
|
||||
height: 128px;
|
||||
background: transparent;
|
||||
private property <length> row-height: 16px;
|
||||
private property <length> label-column-width: 60px;
|
||||
private property <length> loss-column-width: 66px;
|
||||
private property <length> value-column-width:
|
||||
(self.width - self.label-column-width - self.loss-column-width) / 2;
|
||||
|
||||
Rectangle {
|
||||
y: 0px;
|
||||
height: 1px;
|
||||
background: ImGuiLineStyle.border;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
y: 0px;
|
||||
height: stats-table.row-height;
|
||||
Text {
|
||||
x: stats-table.label-column-width + 4px;
|
||||
width: stats-table.value-column-width - 8px;
|
||||
text: StreamStrings.stats-in;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
|
||||
width: stats-table.value-column-width - 8px;
|
||||
text: StreamStrings.stats-out;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
|
||||
width: stats-table.loss-column-width - 8px;
|
||||
text: StreamStrings.stats-loss-rate;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
Rectangle {
|
||||
y: parent.height - 1px;
|
||||
height: 1px;
|
||||
background: ImGuiLineStyle.border;
|
||||
}
|
||||
}
|
||||
|
||||
for row[index] in root.stats-rows: Rectangle {
|
||||
y: (index + 1) * stats-table.row-height;
|
||||
height: stats-table.row-height;
|
||||
Text {
|
||||
x: 4px;
|
||||
width: stats-table.label-column-width - 8px;
|
||||
text: row.label;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
x: stats-table.label-column-width + 4px;
|
||||
width: stats-table.value-column-width - 8px;
|
||||
text: row.inbound;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
Text {
|
||||
x: stats-table.label-column-width + stats-table.value-column-width + 4px;
|
||||
width: stats-table.value-column-width - 8px;
|
||||
text: row.outbound;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
Text {
|
||||
x: stats-table.label-column-width + stats-table.value-column-width * 2 + 4px;
|
||||
width: stats-table.loss-column-width - 8px;
|
||||
text: row.loss-rate;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
Rectangle {
|
||||
y: parent.height - 1px;
|
||||
height: 1px;
|
||||
background: ImGuiLineStyle.border;
|
||||
}
|
||||
}
|
||||
|
||||
for metric[index] in [
|
||||
{ label: "FPS:", value: root.stats-fps },
|
||||
{ label: StreamStrings.stats-resolution + ":", value: root.stats-resolution },
|
||||
{ label: StreamStrings.stats-connection-mode + ":", value: root.stats-connection-mode },
|
||||
]: Rectangle {
|
||||
y: (index + 5) * stats-table.row-height;
|
||||
height: stats-table.row-height;
|
||||
Text {
|
||||
x: 4px;
|
||||
width: stats-table.label-column-width - 8px;
|
||||
text: metric.label;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
x: stats-table.label-column-width + 4px;
|
||||
width: stats-table.value-column-width - 8px;
|
||||
text: metric.value;
|
||||
color: #30343b;
|
||||
font-size: ImGuiFontStyle.small;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
Rectangle {
|
||||
y: parent.height - 1px;
|
||||
height: 1px;
|
||||
background: ImGuiLineStyle.border;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root.display-menu-open: Rectangle {
|
||||
x: control.x + (root.control-docked-left ? 9px : 42px); y: control.y + 40px; width: 180px; height: min(180px, root.displays.length * 30px + 8px);
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
|
||||
VerticalLayout {
|
||||
padding: 4px; spacing: 1px;
|
||||
for display[index] in root.displays: Rectangle {
|
||||
height: 29px;
|
||||
background: display-touch.has-hover || index == root.selected-display ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: display; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
display-touch := TouchArea { clicked => { root.selected-display = index; root.switch-display(index); root.display-menu-open = false; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if root.shortcut-menu-open: Rectangle {
|
||||
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 150px; height: 68px;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
|
||||
VerticalLayout {
|
||||
padding: 4px; spacing: 1px;
|
||||
for shortcut in ["Ctrl+Alt+Del", "Win+L"]: Rectangle {
|
||||
height: 29px; background: shortcut-touch.has-hover ? #e8eef9 : transparent;
|
||||
Text { x: 8px; text: shortcut; color: #30343b; font-size: ImGuiFontStyle.body; vertical-alignment: center; }
|
||||
shortcut-touch := TouchArea { clicked => { root.send-shortcut(shortcut); root.shortcut-menu-open = false; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer progress overlay at bottom-left.
|
||||
if root.file-transfer-visible: Rectangle {
|
||||
x: 18px; y: parent.height - min(210px, parent.height * 0.35); width: min(430px, parent.width * 0.6); height: min(190px, parent.height * 0.32);
|
||||
background: #fffffff0; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 6px; z: 7;
|
||||
Text { x: 12px; y: 8px; text: StreamStrings.file-transfer; color: #2d3138; font-weight: 600; }
|
||||
IconButton { x: parent.width - 32px; y: 1px; icon: FontAwesomeIcons.xmark; icon-size: 13px; clicked => { root.close-file-transfer(); } }
|
||||
ScrollView {
|
||||
x: 10px; y: 32px; width: parent.width - 20px; height: parent.height - 42px;
|
||||
VerticalLayout {
|
||||
// Slint's ScrollView draws its vertical scrollbar over
|
||||
// the viewport. Reserve a gutter so transfer content
|
||||
// never extends underneath it.
|
||||
padding-right: 16px;
|
||||
spacing: 8px;
|
||||
for entry in root.file-transfers: VerticalLayout {
|
||||
spacing: 3px;
|
||||
HorizontalLayout { height: 18px; Text { text: entry.name; color: #30343b; font-size: ImGuiFontStyle.body; overflow: elide; } Text { width: 80px; text: entry.status; color: #5c6470; font-size: ImGuiFontStyle.body; horizontal-alignment: right; } }
|
||||
Rectangle {
|
||||
height: 7px;
|
||||
background: #dfe3e8;
|
||||
border-radius: 3px;
|
||||
|
||||
Rectangle {
|
||||
x: 0px;
|
||||
y: 0px;
|
||||
width: parent.width * max(0, min(1, entry.progress));
|
||||
height: parent.height;
|
||||
background: #2463c7;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
height: 14px;
|
||||
Text { text: round(entry.progress * 100) + "% " + entry.speed; color: #6a717c; font-size: ImGuiFontStyle.small; }
|
||||
Text { text: entry.size; color: #6a717c; font-size: ImGuiFontStyle.small; horizontal-alignment: right; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +123,7 @@ int GuiApplication::AboutWindow() {
|
||||
}
|
||||
|
||||
std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved.";
|
||||
std::string license_text = "Licensed under GNU LGPL v3.";
|
||||
std::string license_text = "Licensed under GNU GPL v3.";
|
||||
ImGui::SetCursorPosX(about_window_width * 0.1f);
|
||||
ImGui::Text("%s", copyright_text.c_str());
|
||||
ImGui::SetCursorPosX(about_window_width * 0.1f);
|
||||
@@ -145,4 +145,4 @@ int GuiApplication::AboutWindow() {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||