Files
crossdesk/apps/desktop/src/gui/application/gui_application.cpp
T

4018 lines
144 KiB
C++

#include "application/gui_application.h"
#include <remote_action.h>
#include <SDL3/SDL.h>
#include <openssl/rand.h>
#include <slint.h>
#include <nlohmann/json.hpp>
#include <tinyfiledialogs.h>
#include <algorithm>
#include <cctype>
#include <charconv>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <random>
#include <shared_mutex>
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
#include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include "localization.h"
#include "platform.h"
#include "platform/video_renderer.h"
#include "rendering/slint_video_presenter.h"
#include "rendering/video_geometry.h"
#include "runtime/privacy_control_state.h"
#if _WIN32
#include <windows.h>
#include "platform/windows/gui/slint_backend.h"
#include "platform/windows/gui/tray/win_tray.h"
#include "platform/windows/gui/window_icons.h"
#elif defined(__APPLE__)
#include "platform/macos/gui/tray/mac_tray.h"
#include "platform/window_drag.h"
#elif defined(__linux__)
#include <GL/gl.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <unistd.h>
#include "platform/linux/gui/cuda_graphics.h"
#include "platform/linux/gui/tray/linux_tray.h"
#include "platform/linux/headless/headless_console.h"
// Xlib defines Status as a macro, which collides with UpdateChecker::Status.
#undef Status
#endif
#include "rd_log.h"
#include "server_window_state.h"
#include "signal_server_settings.h"
#include "signal_server_address.h"
#include "ui/announcement_text.h"
#include "ui/ui_localization.h"
#include "update_checker.h"
#include "window_geometry.h"
namespace crossdesk {
namespace {
using namespace std::chrono_literals;
// Median reconnect to the signal server lands around 7 s on a lossy path, so a
// drop shorter than this is almost always already being recovered from.
constexpr auto kSignalOfflineGrace = 10s;
std::optional<std::string> GenerateRandomPassword(std::string_view current) {
constexpr std::string_view alphabet =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
constexpr auto unbiased_limit = 256 - 256 % alphabet.size();
std::string password;
do {
password.clear();
while (password.size() < 6) {
unsigned char value;
if (RAND_bytes(&value, sizeof(value)) != 1) {
return std::nullopt;
}
// Reject the incomplete bucket so every character is equally likely.
if (value < unbiased_limit) {
password.push_back(alphabet[value % alphabet.size()]);
}
}
} while (password == current ||
password.find_first_not_of("0123456789") == std::string::npos);
return password;
}
std::string CreatePasswordChangeRequestId(uint64_t sequence) {
const auto timestamp = std::chrono::system_clock::now()
.time_since_epoch()
.count();
std::random_device random;
return std::to_string(timestamp) + "-" + std::to_string(random()) + "-" +
std::to_string(sequence);
}
#if defined(__linux__) && !defined(__APPLE__)
bool HasNonEmptyEnvironmentVariable(const char* name) {
const char* value = std::getenv(name);
return value != nullptr && value[0] != '\0';
}
class ScopedUnsetEnvironmentVariable {
public:
explicit ScopedUnsetEnvironmentVariable(const char* name) : name_(name) {
if (const char* value = std::getenv(name_)) {
original_value_ = value;
}
unsetenv(name_);
}
~ScopedUnsetEnvironmentVariable() {
if (original_value_) {
setenv(name_, original_value_->c_str(), 1);
} else {
unsetenv(name_);
}
}
ScopedUnsetEnvironmentVariable(const ScopedUnsetEnvironmentVariable&) =
delete;
ScopedUnsetEnvironmentVariable& operator=(
const ScopedUnsetEnvironmentVariable&) = delete;
private:
const char* name_;
std::optional<std::string> original_value_;
};
::Window X11GetActiveWindow(Display* display) {
if (!display) {
return 0;
}
const ::Window root = DefaultRootWindow(display);
const Atom active_window_atom =
XInternAtom(display, "_NET_ACTIVE_WINDOW", True);
if (active_window_atom != None) {
Atom actual_type = None;
int actual_format = 0;
unsigned long item_count = 0;
unsigned long bytes_after = 0;
unsigned char* data = nullptr;
if (XGetWindowProperty(display, root, active_window_atom, 0, 1, False,
XA_WINDOW, &actual_type, &actual_format, &item_count,
&bytes_after, &data) == Success &&
actual_type == XA_WINDOW && actual_format == 32 && item_count == 1 &&
data) {
const ::Window active_window = *reinterpret_cast<const ::Window*>(data);
XFree(data);
return active_window;
}
if (data) {
XFree(data);
}
}
::Window focused_window = 0;
int revert_to = RevertToNone;
XGetInputFocus(display, &focused_window, &revert_to);
return focused_window == None || focused_window == PointerRoot
? 0
: focused_window;
}
std::optional<unsigned long> X11GetWindowProcessId(Display* display,
::Window x11_window) {
if (!display || !x11_window) {
return std::nullopt;
}
const Atom process_id_atom = XInternAtom(display, "_NET_WM_PID", True);
if (process_id_atom == None) {
return std::nullopt;
}
Atom actual_type = None;
int actual_format = 0;
unsigned long item_count = 0;
unsigned long bytes_after = 0;
unsigned char* data = nullptr;
const int status = XGetWindowProperty(
display, x11_window, process_id_atom, 0, 1, False, XA_CARDINAL,
&actual_type, &actual_format, &item_count, &bytes_after, &data);
if (status != Success || actual_type != XA_CARDINAL || actual_format != 32 ||
item_count != 1 || !data) {
if (data) {
XFree(data);
}
return std::nullopt;
}
const auto process_id = *reinterpret_cast<const unsigned long*>(data);
XFree(data);
return process_id;
}
bool X11WindowMatches(Display* display, ::Window x11_window,
const slint::Window& slint_window) {
if (!display || !x11_window) {
return false;
}
XWindowAttributes attributes;
if (!XGetWindowAttributes(display, x11_window, &attributes)) {
return false;
}
int root_x = 0;
int root_y = 0;
::Window child = 0;
if (!XTranslateCoordinates(display, x11_window, DefaultRootWindow(display), 0,
0, &root_x, &root_y, &child)) {
return false;
}
const auto position = slint_window.position();
const auto size = slint_window.size();
constexpr int kGeometryTolerance = 4;
return std::abs(root_x - position.x) <= kGeometryTolerance &&
std::abs(root_y - position.y) <= kGeometryTolerance &&
std::abs(attributes.width - static_cast<int>(size.width)) <=
kGeometryTolerance &&
std::abs(attributes.height - static_cast<int>(size.height)) <=
kGeometryTolerance;
}
#endif
#if _WIN32
HICON LoadSlintTrayIcon() {
HMODULE module = GetModuleHandleW(nullptr);
HICON icon = reinterpret_cast<HICON>(
LoadImageW(module, L"IDI_ICON1", IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
return icon ? icon : LoadIconW(nullptr, IDI_APPLICATION);
}
#endif
slint::SharedString UiText(std::string_view value) {
return slint::SharedString(value);
}
void RegisterFontAwesome(slint::Window& window) {
if (const auto error = window.window_handle().register_font_from_data(
fa_solid_900_ttf, fa_solid_900_ttf_len)) {
LOG_ERROR("Failed to register Font Awesome for Slint window: {}",
error->data());
}
}
std::string FormatPeerId(const char* id) {
if (!id) {
return {};
}
std::string compact(id);
if (compact.size() != 9) {
return compact;
}
compact.insert(3, " ");
compact.insert(7, " ");
return compact;
}
std::string FormatRemotePeerIdInput(std::string_view input) {
std::string digits;
digits.reserve(9);
for (const unsigned char ch : input) {
if (std::isdigit(ch) != 0 && digits.size() < 9) {
digits.push_back(static_cast<char>(ch));
}
}
if (digits.size() > 3) {
digits.insert(3, " ");
}
if (digits.size() > 7) {
digits.insert(7, " ");
}
return digits;
}
std::string CompactPeerId(std::string id) {
id.erase(
std::remove_if(id.begin(), id.end(),
[](unsigned char ch) { return std::isspace(ch) != 0; }),
id.end());
return id;
}
std::string Trim(std::string value) {
const auto first =
std::find_if_not(value.begin(), value.end(),
[](unsigned char ch) { return std::isspace(ch) != 0; });
const auto last =
std::find_if_not(value.rbegin(), value.rend(), [](unsigned char ch) {
return std::isspace(ch) != 0;
}).base();
if (first >= last) {
return {};
}
return std::string(first, last);
}
struct ParsedReleaseNoteBlock {
slint::StyledText content;
bool section_gap = false;
};
std::vector<ParsedReleaseNoteBlock> ParseReleaseNotesMarkdownForSlint(
std::string_view markdown) {
std::vector<ParsedReleaseNoteBlock> blocks;
bool section_gap = false;
size_t line_start = 0;
while (line_start < markdown.size()) {
const size_t newline = markdown.find('\n', line_start);
const size_t line_end =
newline == std::string_view::npos ? markdown.size() : newline;
std::string_view line = markdown.substr(line_start, line_end - line_start);
if (!line.empty() && line.back() == '\r') {
line.remove_suffix(1);
}
if (std::all_of(line.begin(), line.end(),
[](unsigned char ch) { return std::isspace(ch) != 0; })) {
section_gap = !blocks.empty();
if (newline == std::string_view::npos) {
break;
}
line_start = newline + 1;
continue;
}
size_t heading_end = 0;
while (heading_end < line.size() && heading_end < 6 &&
line[heading_end] == '#') {
++heading_end;
}
const bool is_heading =
heading_end > 0 && heading_end < line.size() &&
std::isspace(static_cast<unsigned char>(line[heading_end])) != 0;
if (is_heading) {
if (newline == std::string_view::npos) {
break;
}
line_start = newline + 1;
continue;
}
const std::string block_markdown(line);
ParsedReleaseNoteBlock block;
if (const auto parsed = slint::StyledText::from_markdown(block_markdown)) {
block.content = *parsed;
} else {
LOG_WARN(
"Failed to parse a release-note Markdown block; using plain text");
block.content = slint::StyledText::from_plain_text(block_markdown);
}
block.section_gap = section_gap;
blocks.push_back(std::move(block));
section_gap = false;
if (newline == std::string_view::npos) {
break;
}
line_start = newline + 1;
}
return blocks;
}
std::string DecodeDroppedPath(std::string text) {
const size_t line_end = text.find_first_of("\r\n");
if (line_end != std::string::npos) {
text.resize(line_end);
}
text = Trim(std::move(text));
if (text.rfind("file://localhost/", 0) == 0) {
text.erase(0, 16);
text.insert(text.begin(), '/');
} else if (text.rfind("file://", 0) == 0) {
text.erase(0, 7);
}
std::string decoded;
decoded.reserve(text.size());
auto hex_value = [](char value) -> int {
if (value >= '0' && value <= '9') return value - '0';
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
return -1;
};
for (size_t i = 0; i < text.size(); ++i) {
if (text[i] == '%' && i + 2 < text.size()) {
const int high = hex_value(text[i + 1]);
const int low = hex_value(text[i + 2]);
if (high >= 0 && low >= 0) {
decoded.push_back(static_cast<char>((high << 4) | low));
i += 2;
continue;
}
}
decoded.push_back(text[i]);
}
#if defined(_WIN32)
if (decoded.size() >= 3 && decoded[0] == '/' &&
std::isalpha(static_cast<unsigned char>(decoded[1])) &&
decoded[2] == ':') {
decoded.erase(decoded.begin());
}
#endif
return decoded;
}
std::optional<int> ParsePort(const slint::SharedString& text) {
const std::string value(text);
int port = 0;
const auto [end, error] =
std::from_chars(value.data(), value.data() + value.size(), port);
if (error != std::errc{} || end != value.data() + value.size() || port <= 0 ||
port > 65535) {
return std::nullopt;
}
return port;
}
std::string ConnectionStatusText(ConnectionStatus status, int language) {
switch (status) {
case ConnectionStatus::Connected:
return localization::p2p_connected[language];
case ConnectionStatus::Connecting:
return localization::p2p_connecting[language];
case ConnectionStatus::Gathering:
return localization::p2p_gathering[language];
case ConnectionStatus::Disconnected:
return localization::p2p_disconnected[language];
case ConnectionStatus::NoSuchTransmissionId:
return localization::no_such_id[language];
case ConnectionStatus::IncorrectPassword:
return localization::reinput_password[language];
case ConnectionStatus::Closed:
return localization::p2p_closed[language];
case ConnectionStatus::RemoteUnavailable:
return localization::device_offline[language];
case ConnectionStatus::Failed:
default:
return localization::p2p_failed[language];
}
}
std::string FormatBitrate(uint32_t bits_per_second) {
char text[48];
if (bits_per_second < 1000) {
std::snprintf(text, sizeof(text), "%u bps", bits_per_second);
} else if (bits_per_second < 1000000) {
std::snprintf(text, sizeof(text), "%u kbps", bits_per_second / 1000);
} else {
std::snprintf(text, sizeof(text), "%.1f mbps", bits_per_second / 1000000.0);
}
return text;
}
std::string FormatTransferRate(uint32_t bits_per_second) {
return bits_per_second == 0 ? std::string{} : FormatBitrate(bits_per_second);
}
std::string FormatLossRate(float loss_rate) {
if (loss_rate < 0.01f) {
return "0%";
}
char text[24];
std::snprintf(text, sizeof(text), "%.0f%%", loss_rate * 100.0f);
return text;
}
std::string FormatFileSize(uint64_t bytes) {
char text[48];
if (bytes < 1024) {
std::snprintf(text, sizeof(text), "%llu B",
static_cast<unsigned long long>(bytes));
} else if (bytes < 1024 * 1024) {
std::snprintf(text, sizeof(text), "%.2f KB", bytes / 1024.0);
} else if (bytes < 1024ULL * 1024ULL * 1024ULL) {
std::snprintf(text, sizeof(text), "%.2f MB", bytes / (1024.0 * 1024.0));
} else {
std::snprintf(text, sizeof(text), "%.2f GB",
bytes / (1024.0 * 1024.0 * 1024.0));
}
return text;
}
uint32_t DecodeFirstUtf8CodePoint(const std::string& text) {
if (text.empty()) {
return 0;
}
const auto* bytes = reinterpret_cast<const unsigned char*>(text.data());
if (bytes[0] < 0x80) {
return bytes[0];
}
if ((bytes[0] & 0xE0) == 0xC0 && text.size() >= 2) {
return ((bytes[0] & 0x1F) << 6) | (bytes[1] & 0x3F);
}
if ((bytes[0] & 0xF0) == 0xE0 && text.size() >= 3) {
return ((bytes[0] & 0x0F) << 12) | ((bytes[1] & 0x3F) << 6) |
(bytes[2] & 0x3F);
}
if ((bytes[0] & 0xF8) == 0xF0 && text.size() >= 4) {
return ((bytes[0] & 0x07) << 18) | ((bytes[1] & 0x3F) << 12) |
((bytes[2] & 0x3F) << 6) | (bytes[3] & 0x3F);
}
return 0;
}
int SlintKeyToWindowsVk(const std::string& text) {
const uint32_t key = DecodeFirstUtf8CodePoint(text);
if (key >= 'a' && key <= 'z') {
return static_cast<int>(key - 'a' + 'A');
}
if ((key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) {
return static_cast<int>(key);
}
if (key >= 0xF704 && key <= 0xF71B) {
return 0x70 + static_cast<int>(key - 0xF704);
}
switch (key) {
case 0x08:
return 0x08;
case 0x09:
case 0x19:
return 0x09;
case 0x0A:
return 0x0D;
case 0x1B:
return 0x1B;
case 0x7F:
return 0x2E;
case 0x10:
return 0xA0;
case 0x15:
return 0xA1;
case 0x11:
return 0xA2;
case 0x16:
return 0xA3;
case 0x12:
return 0xA4;
case 0x13:
return 0xA5;
case 0x17:
return 0x5B;
case 0x18:
return 0x5C;
case 0x14:
return 0x14;
case 0x20:
return 0x20;
case 0xF700:
return 0x26;
case 0xF701:
return 0x28;
case 0xF702:
return 0x25;
case 0xF703:
return 0x27;
case 0xF727:
return 0x2D;
case 0xF729:
return 0x24;
case 0xF72B:
return 0x23;
case 0xF72C:
return 0x21;
case 0xF72D:
return 0x22;
case 0xF72F:
return 0x91;
case 0xF730:
return 0x13;
case 0xF731:
return 0x2C;
case 0xF734:
return 0xB2;
case 0xF735:
return 0x5D;
case 0xF748:
return 0xA6;
case ';':
case ':':
return 0xBA;
case '\'':
case '"':
return 0xDE;
case '`':
case '~':
return 0xC0;
case ',':
case '<':
return 0xBC;
case '.':
case '>':
return 0xBE;
case '/':
case '?':
return 0xBF;
case '\\':
case '|':
return 0xDC;
case '[':
case '{':
return 0xDB;
case ']':
case '}':
return 0xDD;
case '-':
case '_':
return 0xBD;
case '=':
case '+':
return 0xBB;
case '!':
return '1';
case '@':
return '2';
case '#':
return '3';
case '$':
return '4';
case '%':
return '5';
case '^':
return '6';
case '&':
return '7';
case '*':
return '8';
case '(':
return '9';
case ')':
return '0';
default:
return -1;
}
}
struct WindowDragState {
bool active = false;
bool use_global_pointer = false;
float pointer_start_x = 0;
float pointer_start_y = 0;
float last_local_x = 0;
float last_local_y = 0;
float global_to_physical_scale = 1;
slint::PhysicalPosition window_start;
slint::PhysicalPosition last_target;
#if defined(__linux__) && !defined(__APPLE__)
std::unique_ptr<Display, decltype(&XCloseDisplay)> x11_display{nullptr,
XCloseDisplay};
slint::Timer pointer_timer;
#endif
bool ReadGlobalPointer(float* x, float* y, bool* left_pressed = nullptr) {
#if defined(__linux__) && !defined(__APPLE__)
const char* driver = SDL_GetCurrentVideoDriver();
if (driver && std::strcmp(driver, "x11") == 0) {
// Slint can dispatch motion before SDL pumps its XInput2 events, and
// pointer warps do not emit raw motion at all. Query X11 directly so
// dragging never depends on SDL's cached global pointer position.
if (!x11_display) {
x11_display.reset(XOpenDisplay(nullptr));
}
if (!x11_display) {
return false;
}
::Window root, child;
int root_x, root_y, local_x, local_y;
unsigned int buttons;
if (!XQueryPointer(x11_display.get(), DefaultRootWindow(x11_display.get()),
&root, &child, &root_x, &root_y, &local_x, &local_y,
&buttons)) {
return false;
}
if (left_pressed) *left_pressed = (buttons & Button1Mask) != 0;
*x = static_cast<float>(root_x);
*y = static_cast<float>(root_y);
return true;
}
#endif
const auto buttons = SDL_GetGlobalMouseState(x, y);
if (left_pressed) *left_pressed = (buttons & SDL_BUTTON_LMASK) != 0;
return true;
}
};
bool CanUseGlobalPointerPosition() {
const char* driver = SDL_GetCurrentVideoDriver();
if (driver == nullptr) {
return false;
}
#if defined(__linux__) && !defined(__APPLE__)
// Wayland deliberately does not expose global pointer coordinates or allow
// clients to position top-level windows.
return std::strcmp(driver, "wayland") != 0;
#else
return true;
#endif
}
constexpr float kStreamWindowLogicalWidth = 1280.0f;
constexpr float kStreamWindowLogicalHeight = 720.0f;
constexpr float kWaylandTitlebarLogicalHeight = 32.0f;
float StreamWindowLogicalHeight(bool custom_titlebar) {
return kStreamWindowLogicalHeight +
(custom_titlebar ? kWaylandTitlebarLogicalHeight : 0.0f);
}
SDL_DisplayID DisplayForSlintWindow(const slint::Window& window) {
const auto position = window.position();
const auto size = window.size();
#if defined(__APPLE__)
const float scale = std::max(window.scale_factor(), 0.01f);
const SDL_Point center{
static_cast<int>(
std::lround(position.x / scale + size.width / (2.0f * scale))),
static_cast<int>(
std::lround(position.y / scale + size.height / (2.0f * scale)))};
#else
const SDL_Point center{
position.x + static_cast<int>(size.width / 2),
position.y + static_cast<int>(size.height / 2)};
#endif
const SDL_DisplayID display = SDL_GetDisplayForPoint(&center);
return display != 0 ? display : SDL_GetPrimaryDisplay();
}
#if !defined(__APPLE__)
window_geometry::PhysicalSize PhysicalWindowSize(const slint::Window& window,
float logical_width,
float logical_height) {
const auto size = window.size();
if (size.width > 0 && size.height > 0) {
return {static_cast<int>(size.width), static_cast<int>(size.height)};
}
const float scale = std::max(window.scale_factor(), 0.01f);
return {std::max(1, static_cast<int>(std::lround(logical_width * scale))),
std::max(1, static_cast<int>(std::lround(logical_height * scale)))};
}
window_geometry::PhysicalRect PhysicalDisplayBounds(const SDL_Rect& bounds) {
return {bounds.x, bounds.y, bounds.w, bounds.h};
}
#endif
bool PositionWindowAtCenter(slint::Window& window, const slint::Window& anchor,
float logical_width, float logical_height) {
if (!CanUseGlobalPointerPosition()) {
return false;
}
const SDL_DisplayID display = DisplayForSlintWindow(anchor);
SDL_Rect usable_bounds{};
if (display == 0 || !SDL_GetDisplayUsableBounds(display, &usable_bounds)) {
LOG_WARN("Unable to obtain usable display bounds for stream window: {}",
SDL_GetError());
return false;
}
#if defined(__APPLE__)
const float target_x =
static_cast<float>(usable_bounds.x) +
(static_cast<float>(usable_bounds.w) - logical_width) / 2.0f;
const float target_y =
static_cast<float>(usable_bounds.y) +
(static_cast<float>(usable_bounds.h) - logical_height) / 2.0f;
window.set_position(
slint::LogicalPosition(slint::Point<float>{target_x, target_y}));
#else
const auto target = window_geometry::CenteredPosition(
PhysicalDisplayBounds(usable_bounds),
PhysicalWindowSize(window, logical_width, logical_height));
window.set_position(slint::PhysicalPosition(
slint::Point<int32_t>{target.x, target.y}));
#endif
return true;
}
#if _WIN32
bool ConfigureWindowsServerWindow(slint::Window& window) {
const HWND hwnd = window.win32_hwnd();
if (!hwnd) {
return false;
}
LONG_PTR ex_style = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
ex_style |= WS_EX_TOOLWINDOW;
ex_style &= ~WS_EX_APPWINDOW;
SetWindowLongPtr(hwnd, GWL_EXSTYLE, ex_style);
return SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED |
SWP_NOACTIVATE) != FALSE;
}
#endif
template <typename WindowHandle>
void DragWindow(WindowHandle& component, int phase, float mouse_x,
float mouse_y, WindowDragState& state) {
if (phase == 1) {
#if defined(__APPLE__)
// Let AppKit run the native move loop. This preserves macOS window
// snapping, Spaces, multi-display, and high-DPI behavior without manually
// updating the window position for every pointer event.
if (!StartNativeWindowDrag()) {
LOG_WARN("Unable to start native macOS window drag");
}
state.active = false;
return;
#endif
auto& window = component->window();
state.active = true;
state.last_local_x = mouse_x;
state.last_local_y = mouse_y;
state.window_start = window.position();
state.last_target = state.window_start;
state.use_global_pointer =
CanUseGlobalPointerPosition() &&
state.ReadGlobalPointer(&state.pointer_start_x, &state.pointer_start_y);
if (state.use_global_pointer) {
#if defined(__APPLE__)
// Cocoa reports global pointer positions in logical screen points,
// while Slint/winit accepts physical window positions.
state.global_to_physical_scale = window.scale_factor();
#else
state.global_to_physical_scale = 1.0f;
#endif
}
#if defined(__linux__) && !defined(__APPLE__)
if (state.use_global_pointer && state.x11_display) {
// Moving the window can leave consecutive pointer events at identical
// local coordinates, so Slint need not emit another TouchArea::moved.
// Follow the global pointer throughout the drag, including those moves.
state.pointer_timer.start(
slint::TimerMode::Repeated, std::chrono::milliseconds(16),
[weak = slint::ComponentWeakHandle(component), &state] {
if (auto window = weak.lock()) {
DragWindow(*window, 2, 0, 0, state);
} else {
state.active = false;
state.pointer_timer.stop();
}
});
}
#endif
return;
}
if (phase == 0) {
#if defined(__linux__) && !defined(__APPLE__)
state.pointer_timer.stop();
if (state.use_global_pointer && state.x11_display) {
DragWindow(component, 2, mouse_x, mouse_y, state);
}
#endif
state.active = false;
return;
}
if (!state.active || phase != 2) {
return;
}
auto& window = component->window();
if (state.use_global_pointer) {
float pointer_x = 0;
float pointer_y = 0;
if (!state.ReadGlobalPointer(&pointer_x, &pointer_y)) {
return;
}
const int delta_x = static_cast<int>(std::lround(
(pointer_x - state.pointer_start_x) * state.global_to_physical_scale));
const int delta_y = static_cast<int>(std::lround(
(pointer_y - state.pointer_start_y) * state.global_to_physical_scale));
const slint::PhysicalPosition target(slint::Point<int32_t>{
state.window_start.x + delta_x, state.window_start.y + delta_y});
if (target.x != state.last_target.x || target.y != state.last_target.y) {
window.set_position(target);
state.last_target = target;
}
return;
}
// Retain the previous behavior for backends without global pointer access.
const float scale = window.scale_factor();
const auto position = window.position();
const int delta_x =
static_cast<int>(std::lround((mouse_x - state.last_local_x) * scale));
const int delta_y =
static_cast<int>(std::lround((mouse_y - state.last_local_y) * scale));
if (delta_x != 0 || delta_y != 0) {
window.set_position(slint::PhysicalPosition(
slint::Point<int32_t>{position.x + delta_x, position.y + delta_y}));
}
state.last_local_x = mouse_x;
state.last_local_y = mouse_y;
}
} // namespace
struct GuiApplication::SlintUi {
uint64_t announcement_version = UINT64_MAX;
std::string announcement_selected_id, announcement_selected_revision;
slint::ComponentHandle<ui::MainWindow> main = ui::MainWindow::create();
std::shared_ptr<slint::VectorModel<ui::ReleaseNoteBlock>>
release_note_blocks_model =
std::make_shared<slint::VectorModel<ui::ReleaseNoteBlock>>();
std::optional<slint::ComponentHandle<ui::StreamWindow>> stream;
std::optional<slint::ComponentHandle<ui::ServerWindow>> server;
std::optional<slint::ComponentHandle<ui::ServerStatusBar>> server_bar;
std::shared_ptr<slint::VectorModel<ui::RecentConnection>> recent_model =
std::make_shared<slint::VectorModel<ui::RecentConnection>>();
std::shared_ptr<slint::VectorModel<ui::StreamTab>> tab_model =
std::make_shared<slint::VectorModel<ui::StreamTab>>();
std::shared_ptr<slint::VectorModel<slint::SharedString>> display_model =
std::make_shared<slint::VectorModel<slint::SharedString>>();
std::shared_ptr<slint::VectorModel<ui::FileTransferEntry>> transfer_model =
std::make_shared<slint::VectorModel<ui::FileTransferEntry>>();
std::shared_ptr<slint::VectorModel<ui::NetworkStatsRow>> stats_model =
std::make_shared<slint::VectorModel<ui::NetworkStatsRow>>();
std::shared_ptr<slint::VectorModel<ui::ControllerEntry>> controller_model =
std::make_shared<slint::VectorModel<ui::ControllerEntry>>();
std::unique_ptr<SlintVideoPresenter> video_presenter;
std::vector<std::string> tab_order;
std::vector<std::string> tab_ids;
std::string tab_model_signature;
std::vector<std::string> controller_ids;
std::string controller_model_signature;
std::string connection_dialog_remote_id;
bool connection_password_initialized = false;
bool portable_service_dialog_initialized = false;
std::string recent_model_signature;
slint::Timer timer;
slint::Timer server_file_dialog_timer;
WindowDragState main_drag;
WindowDragState stream_drag;
WindowDragState server_drag;
int main_native_titlebar_attempts = 30;
int stream_live_resize_configuration_attempts = 0;
int stream_initial_position_attempts = 0;
SDL_DisplayID server_display = 0;
float server_anchor = 0.3f;
float server_drag_anchor = 0.3f;
bool server_drag_moved = false;
bool server_pointer_pressed = false;
bool server_file_dialog_open = false;
bool server_suspended_for_dialog = false;
server_window_state::EdgeLayout server_layout{};
#if _WIN32
int server_native_window_attempts = 0;
#endif
int localized_language = -1;
std::chrono::steady_clock::time_point last_clipboard_poll{};
slint::Timer video_timer;
#if _WIN32
std::unique_ptr<WinTray> tray;
#elif defined(__APPLE__)
std::unique_ptr<MacTray> tray;
#elif defined(__linux__)
std::unique_ptr<LinuxTray> tray;
Display* x11_focus_display = nullptr;
std::chrono::steady_clock::time_point last_x11_activation_poll{};
#endif
~SlintUi() {
#if defined(__linux__) && !defined(__APPLE__)
if (x11_focus_display) {
XCloseDisplay(x11_focus_display);
}
#endif
}
};
GuiApplication::GuiApplication() = default;
GuiApplication::~GuiApplication() = default;
int GuiApplication::Run() {
path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (!path_manager_) {
return -1;
}
exec_log_path_ = path_manager_->GetLogPath().string();
dll_log_path_ = exec_log_path_;
InitializeLogger();
cache_path_ = path_manager_->GetCachePath().string();
announcements_.SetStorageDirectory(path_manager_->GetCachePath() /
"announcement_reads");
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
sizeof(signal_server_ip_self_) - 1);
const int signal_port = config_center_->GetSignalServerPort();
if (signal_port > 0 && signal_port <= 65535) {
std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_),
"%d", signal_port);
}
InitializeSettings();
#ifdef __linux__
HeadlessConsole::Instance().BindSettings(
config_center_.get(), [this] {
std::lock_guard<std::mutex> lock(password_change_mutex_);
return HasActiveSession() || password_change_pending_ || credential_recovery_in_progress_;
}, [this](SettingEffect effect) {
ApplySettingsFromConfig(effect == SettingEffect::reconnect);
});
#endif
if (!InitializeSDL()) {
return -1;
}
#if _WIN32
const auto backend = ConfigureWindowsSlintBackend();
if (!backend.success) {
LOG_ERROR("{}", backend.diagnostic);
SDL_Quit();
return -1;
}
LOG_INFO("Slint backend: {}; {}", backend.backend, backend.diagnostic);
#endif
InitializeModules();
update_checker_ = std::make_unique<UpdateChecker>(CROSSDESK_VERSION);
InitializeUi();
ui_->timer.start(slint::TimerMode::Repeated, 16ms, [this] { Tick(); });
ScheduleNextVideoFrame();
// The native tray icon is managed outside Slint, so Slint cannot count it
// when deciding whether the last hidden window should stop the event loop.
// Keep the loop alive until the tray's explicit Exit action requests quit.
ui_->main->show();
#if _WIN32
ConfigureWindowsWindowIcons(ui_->main->window().win32_hwnd());
#elif defined(__APPLE__)
ConfigureMainWindowChrome(ui_->main->window().appkit_view());
#endif
slint::run_event_loop(slint::EventLoopMode::RunUntilQuit);
Cleanup();
return 0;
}
void GuiApplication::InitializeLogger() { InitLogger(exec_log_path_); }
void GuiApplication::InitializeSettings() {
settings_.Load();
if (thumbnail_->SetSavePreviews(config_center_->IsSaveRemotePreviews()) != 0) {
LOG_ERROR("Failed to clear saved remote previews");
}
settings_.LoadRecentConnectionAliases();
localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_);
language_button_value_ = localization_language_index_;
localization_language_ =
static_cast<ConfigCenter::LANGUAGE>(localization_language_index_);
privacy_.SetText({
localization::privacy_screen_unlock_hint[localization_language_index_]});
}
bool GuiApplication::InitializeSDL() {
#if defined(__linux__) && !defined(__APPLE__)
ConfigureLinuxCudaGraphics(config_center_->IsHardwareVideoCodec());
// Standard Wayland top-level windows cannot choose an absolute screen
// position. Use the session's XWayland compatibility server for the GUI so
// auxiliary windows such as the controlled-side status window can be
// placed deterministically. Capture and input still detect the surrounding
// Wayland session through XDG_SESSION_TYPE and continue to use the portal.
const char* requested_video_driver = std::getenv("SDL_VIDEODRIVER");
const bool video_driver_allows_x11 =
requested_video_driver == nullptr || requested_video_driver[0] == '\0' ||
std::strcmp(requested_video_driver, "x11") == 0;
use_xwayland_gui_ = IsWaylandSession() &&
HasNonEmptyEnvironmentVariable("DISPLAY") &&
video_driver_allows_x11;
if (use_xwayland_gui_) {
setenv("SDL_VIDEODRIVER", "x11", 1);
LOG_INFO(
"Wayland session detected; using XWayland for positioned GUI "
"windows while retaining Wayland portal capture and input");
}
#endif
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
return false;
}
#if defined(__linux__) && !defined(__APPLE__)
const char* active_video_driver = SDL_GetCurrentVideoDriver();
// X11 permits the manual move/position operations used by the frameless
// main window. Keep native Wayland decorations as a compatibility fallback.
use_x11_custom_titlebar_ =
active_video_driver != nullptr &&
std::strcmp(active_video_driver, "x11") == 0;
#endif
if (const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(0)) {
screen_width_ = mode->w;
screen_height_ = mode->h;
}
clipboard_.Initialize();
return true;
}
void GuiApplication::InitializeModules() {
if (modules_inited_) {
return;
}
// Renderer construction can log device, shader, or pipeline failures.
// Initialize it after Run() has selected the application's log directory.
video_renderer_ = CreateVideoRenderer();
devices_.Initialize();
CreateConnectionPeer();
modules_inited_ = true;
}
void GuiApplication::InitializeUi() {
#if defined(__APPLE__)
// Slint's renderer must be selected before the first component creates the
// process-wide backend. The macOS package contains Skia (Metal) and the
// software fallback, but no OpenGL window renderer.
const char* requested_slint_backend = std::getenv("SLINT_BACKEND");
if (requested_slint_backend == nullptr ||
requested_slint_backend[0] == '\0') {
if (setenv("SLINT_BACKEND", "winit-skia", 1) == 0) {
LOG_INFO("Using Slint winit-skia renderer with the macOS Metal backend");
} else {
LOG_WARN("Unable to select the Slint Metal backend through SLINT_BACKEND");
}
} else {
LOG_INFO("Using requested Slint backend: {}", requested_slint_backend);
}
#endif
#if defined(__linux__) && !defined(__APPLE__)
// Detect the desktop session before temporarily hiding Wayland variables
// for the XWayland GUI backend below.
const bool is_wayland_session = IsWaylandSession();
// Winit prefers Wayland whenever WAYLAND_DISPLAY/WAYLAND_SOCKET is present.
// Hide them only while Slint selects its process-wide window backend, then
// restore the environment before the rest of the application starts.
std::optional<ScopedUnsetEnvironmentVariable> wayland_display_guard;
std::optional<ScopedUnsetEnvironmentVariable> wayland_socket_guard;
if (use_xwayland_gui_) {
wayland_display_guard.emplace("WAYLAND_DISPLAY");
wayland_socket_guard.emplace("WAYLAND_SOCKET");
}
// Match crossdesk.desktop even when the Debian launcher runs crossdesk-bin.
// Set this before creating any windows so X11 and Wayland share the same id.
slint::set_xdg_app_id("crossdesk");
#endif
ui_ = std::make_unique<SlintUi>();
ui_->video_presenter =
std::make_unique<SlintVideoPresenter>(*video_renderer_);
#if _WIN32
ui_->main->set_custom_titlebar(true);
#elif defined(__linux__)
ui_->main->set_custom_titlebar(use_x11_custom_titlebar_);
ui_->main->set_wayland_titlebar(use_xwayland_gui_);
// Set this only at startup so dismissing the notice lasts until exit.
ui_->main->set_wayland_notice_open(is_wayland_session);
#endif
ui_->main->set_release_note_blocks(ui_->release_note_blocks_model);
RegisterFontAwesome(ui_->main->window());
ui_->main->set_recent_connections(ui_->recent_model);
ResetSettingsUi();
BindMainCallbacks();
InitializeSystemTray();
UpdateLocalization();
SyncMainWindow();
}
void GuiApplication::InitializeSystemTray() {
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
if (!ui_) {
return;
}
auto show_window = [this] {
if (!ui_) {
return;
}
ui_->main->show();
#if _WIN32
ConfigureWindowsWindowIcons(ui_->main->window().win32_hwnd());
#endif
ui_->main->window().set_minimized(false);
#if defined(__APPLE__)
ConfigureMainWindowChrome(ui_->main->window().appkit_view());
MacActivateWindow(ui_->main->window().appkit_view());
// A window restored from the tray may only obtain its NSWindow after the
// callback returns, so let the regular UI tick configure it.
ui_->main_native_titlebar_attempts = 30;
#endif
};
auto hide_window = [this] {
if (ui_) {
ui_->main->hide();
}
};
auto open_settings = [this, show_window] {
if (!ui_) {
return;
}
show_window();
ui_->main->set_settings_open(true);
};
auto exit_app = [this] {
exit_ = true;
slint::quit_event_loop();
};
#if _WIN32
ui_->tray = std::make_unique<WinTray>(
std::move(show_window), std::move(hide_window),
std::move(open_settings), std::move(exit_app), LoadSlintTrayIcon(),
L"CrossDesk", localization_language_index_);
#elif defined(__APPLE__)
ui_->tray = std::make_unique<MacTray>(
std::move(show_window), std::move(hide_window), std::move(open_settings),
std::move(exit_app), "CrossDesk", localization_language_index_);
#elif defined(__linux__)
ui_->tray = std::make_unique<LinuxTray>(
std::move(show_window), std::move(hide_window),
std::move(open_settings), std::move(exit_app), "CrossDesk",
localization_language_index_);
#endif
#endif
}
bool GuiApplication::MinimizeMainWindowToTray() {
if (!ui_) {
return false;
}
#if _WIN32 || defined(__APPLE__)
if (!ui_->tray) {
return false;
}
ui_->tray->MinimizeToTray();
return true;
#elif defined(__linux__)
return ui_->tray && ui_->tray->MinimizeToTray();
#else
return false;
#endif
}
void GuiApplication::ResetSettingsUi() {
if (!ui_) {
return;
}
ui_->main->set_language_index(language_button_value_);
ui_->main->set_codec_index(video_encode_format_button_value_);
const bool hardware_codec_available =
ConfigCenter::IsHardwareVideoCodecAvailable();
ui_->main->set_hardware_codec_available(hardware_codec_available);
ui_->main->set_hardware_codec_enabled(hardware_codec_available &&
enable_hardware_video_codec_);
ui_->main->set_turn_enabled(enable_turn_);
ui_->main->set_force_relay_enabled(config_center_->IsForceRelay());
ui_->main->set_self_hosted_enabled(enable_self_hosted_);
ui_->main->set_autostart_enabled(enable_autostart_);
ui_->main->set_daemon_enabled(enable_daemon_);
ui_->main->set_save_remote_previews(config_center_->IsSaveRemotePreviews());
ui_->main->set_privacy_on_connect_enabled(
config_center_->IsEnablePrivacyScreen());
#ifdef _WIN32
ui_->main->set_capture_method_visible(true);
ui_->main->set_capture_method_index(
static_cast<int>(config_center_->GetScreenCaptureMethod()));
#endif
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
ui_->main->set_privacy_setting_available(true);
#else
ui_->main->set_privacy_setting_available(false);
#endif
ui_->main->set_file_save_path(file_transfer_save_path_buf_);
ui_->main->set_server_host(signal_server_ip_self_);
ui_->main->set_server_port(signal_server_port_self_);
ui_->main->set_self_host_settings_error("");
}
void GuiApplication::BindMainCallbacks() {
ui_->main->on_delete_announcement([this](slint::SharedString id,
slint::SharedString revision) {
const auto parse = [](const slint::SharedString& value)
-> std::optional<int64_t> {
const std::string_view text(value.data());
int64_t number = 0;
const auto result =
std::from_chars(text.data(), text.data() + text.size(), number);
if (result.ec != std::errc{} ||
result.ptr != text.data() + text.size() || number <= 0)
return std::nullopt;
return number;
};
const auto announcement_id = parse(id);
const auto announcement_revision = parse(revision);
if (!announcement_id || !announcement_revision) return;
announcements_.Dismiss(*announcement_id, *announcement_revision);
SyncAnnouncements();
});
ui_->main->on_refresh_announcements([this] { announcements_.Refresh(); });
ui_->main->on_open_announcement_link([this](slint::SharedString link) {
const std::string url(link);
if (announcement_text::IsWebUrl(url)) OpenUrl(url);
});
ui_->main->on_load_more_announcements([this] {
announcements_.LoadMore();
SyncAnnouncements();
});
ui_->main->on_open_announcement([this](slint::SharedString id,
slint::SharedString revision) {
const auto snapshot = announcements_.Get();
for (const auto& item : snapshot.items) {
if (std::to_string(item["id"].get<int64_t>()) != id.data() ||
std::to_string(item["revision"].get<int64_t>()) != revision.data())
continue;
ui_->announcement_selected_id = id.data();
ui_->announcement_selected_revision = revision.data();
ui_->announcement_version = UINT64_MAX;
announcements_.Read(item["id"], item["revision"]);
SyncAnnouncements();
break;
}
});
auto& main = ui_->main;
main->window().on_close_requested([this] {
// Keep the application and all active sessions alive. The tray helper
// ensures that supported platforms still provide a way to restore the
// main window; Slint hides it regardless of tray availability.
MinimizeMainWindowToTray();
return slint::CloseRequestResponse::HideWindow;
});
main->on_main_title_drag([this](int phase, float x, float y) {
if (ui_) {
DragWindow(ui_->main, phase, x, y, ui_->main_drag);
}
});
main->on_minimize_main_window([this] {
if (ui_) {
ui_->main->window().set_minimized(true);
}
});
main->on_close_main_window([this] {
if (!ui_) {
return;
}
MinimizeMainWindowToTray();
ui_->main->hide();
});
main->on_copy_local_id([this] {
if (!SDL_SetClipboardText(client_id_)) {
LOG_WARN("Copy local id failed: {}", SDL_GetError());
}
});
main->on_toggle_password_visibility([this] {
show_password_ = !show_password_;
ui_->main->set_password_visible(show_password_);
});
main->on_reset_password([this](slint::SharedString value) {
return RequestPasswordChange(std::string(value));
});
main->on_refresh_password([this] {
const auto password = GenerateRandomPassword(password_saved_);
if (!password) {
LOG_ERROR("Could not generate a random password");
offline_warning_text_ = localization::failed[localization_language_index_];
show_offline_warning_window_ = true;
return;
}
RequestPasswordChange(*password);
});
main->on_connect_requested(
[this](slint::SharedString id) { ConnectFromUi(std::string(id)); });
main->on_format_remote_id([](slint::SharedString value) {
return UiText(FormatRemotePeerIdInput(std::string_view(value)));
});
main->on_recent_connect(
[this](slint::SharedString id) { ConnectFromUi(std::string(id)); });
main->on_recent_edit_alias(
[this](slint::SharedString id, slint::SharedString alias) {
const std::string remote_id(id);
const std::string trimmed = Trim(std::string(alias));
if (trimmed.empty()) {
settings_.EraseRecentConnectionAlias(remote_id);
} else {
settings_.SetRecentConnectionAlias(remote_id, trimmed);
}
settings_.SaveRecentConnectionAliases();
reload_recent_connections_ = true;
});
main->on_recent_delete([this](slint::SharedString id) {
const std::string remote_id(id);
const auto connection = std::find_if(
recent_connections_.begin(), recent_connections_.end(),
[&](const auto& entry) { return entry.second.remote_id == remote_id; });
if (connection != recent_connections_.end()) {
thumbnail_->DeleteThumbnail(connection->first);
settings_.EraseRecentConnectionAlias(remote_id);
settings_.SaveRecentConnectionAliases();
reload_recent_connections_ = true;
}
});
main->on_browse_save_path([this] {
const char* folder = tinyfd_selectFolderDialog(
localization::file_transfer_save_path[localization_language_index_]
.c_str(),
file_transfer_save_path_buf_[0] ? file_transfer_save_path_buf_
: nullptr);
if (folder) {
ui_->main->set_file_save_path(folder);
}
});
main->on_save_settings([this] { return SaveSettingsFromUi(); });
main->on_cancel_settings([this] { ResetSettingsUi(); });
main->on_save_self_hosted_settings([this] {
// Stage the endpoint until the surrounding settings dialog is confirmed.
return ValidateSelfHostedSettingsFromUi();
});
main->on_cancel_self_hosted_settings([this] {
ui_->main->set_self_host_settings_error("");
const std::string host = config_center_->GetSignalServerHost();
ui_->main->set_server_host(UiText(host));
const int signal_port = config_center_->GetSignalServerPort();
ui_->main->set_server_port(signal_port > 0
? UiText(std::to_string(signal_port))
: slint::SharedString{});
});
main->on_open_download([this] {
const auto& latest = update_checker_->latest();
OpenUrl(latest && !latest->download_url.empty() ? latest->download_url
: "https://crossdesk.cn");
});
main->on_check_for_updates([this] {
update_checker_->RequestCheck();
SyncMainWindow();
});
main->on_connection_cancel([this] {
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
if (!props) {
show_connection_status_window_ = false;
return;
}
CloseStreamTab(props->remote_id_);
re_enter_remote_id_ = true;
show_connection_status_window_ = false;
});
main->on_connection_acknowledge([this] {
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
if (props) {
const ConnectionStatus status = props->connection_status_.load();
if (status == ConnectionStatus::NoSuchTransmissionId ||
status == ConnectionStatus::RemoteUnavailable) {
CloseStreamTab(props->remote_id_);
re_enter_remote_id_ = true;
}
}
show_connection_status_window_ = false;
});
main->on_connection_submit_password([this](slint::SharedString value,
bool remember) {
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
const std::string password(value);
if (!props || password.empty() || password.size() > 6) {
return;
}
if (!props->connection_auth_.Begin(password)) return;
std::memset(props->remote_password_, 0, sizeof(props->remote_password_));
std::memcpy(props->remote_password_, password.data(), password.size());
props->remember_password_ = remember;
props->rejoin_ = true;
need_to_rejoin_ = true;
});
main->on_request_screen_recording_permission([this] {
#ifdef __APPLE__
OpenScreenRecordingPreferences();
mac_screen_recording_permission_requested_ = true;
RefreshMacPermissionStatus(true);
#endif
});
main->on_request_accessibility_permission([this] {
#ifdef __APPLE__
OpenAccessibilityPreferences();
mac_accessibility_permission_requested_ = true;
RefreshMacPermissionStatus(true);
#endif
});
main->on_install_portable_service([this] {
#if _WIN32 && CROSSDESK_PORTABLE
portable_service_do_not_remind_ = false;
ui_->main->set_portable_service_do_not_remind(false);
StartPortableWindowsServiceInstall();
#endif
});
main->on_cancel_portable_service([this] {
#if _WIN32 && CROSSDESK_PORTABLE
if (portable_service_install_state_.load(std::memory_order_acquire) ==
PortableServiceInstallState::installing) {
return;
}
portable_service_do_not_remind_ =
ui_->main->get_portable_service_do_not_remind();
if (portable_service_do_not_remind_) {
portable_service_prompt_suppressed_ = true;
config_center_->SetPortableServicePromptSuppressed(true);
show_portable_service_prompt_suppressed_window_ = true;
}
show_portable_service_install_window_ = false;
#endif
});
main->on_acknowledge_portable_service([this] {
#if _WIN32 && CROSSDESK_PORTABLE
show_portable_service_install_window_ = false;
portable_service_installed_ =
portable_service_install_state_.load(std::memory_order_acquire) ==
PortableServiceInstallState::succeeded;
JoinPortableWindowsServiceInstallThread();
#endif
});
main->on_acknowledge_portable_service_suppressed([this] {
#if _WIN32 && CROSSDESK_PORTABLE
show_portable_service_prompt_suppressed_window_ = false;
#endif
});
}
void GuiApplication::BindStreamCallbacks() {
if (!ui_->stream) {
return;
}
auto& stream = *ui_->stream;
stream->on_dismiss_remote_update([this] {
if (auto props = SelectedSession()) {
std::lock_guard lock(props->remote_version_mutex_);
props->remote_update_dismissed_ = true;
}
});
auto close_stream_sessions = [this] {
std::vector<std::string> ids;
{
std::shared_lock lock(remote_sessions_mutex_);
for (const auto& [id, _] : remote_sessions_) {
ids.push_back(id);
}
}
for (const auto& id : ids) {
CloseStreamTab(id);
}
if (ui_ && ui_->stream) {
(*ui_->stream)->hide();
}
};
stream->window().on_close_requested([close_stream_sessions] {
close_stream_sessions();
return slint::CloseRequestResponse::HideWindow;
});
stream->on_stream_title_drag([this](int phase, float x, float y) {
if (ui_ && ui_->stream) {
DragWindow(*ui_->stream, phase, x, y, ui_->stream_drag);
}
});
stream->on_minimize_stream_window([this] {
if (ui_ && ui_->stream) {
(*ui_->stream)->window().set_minimized(true);
}
});
stream->on_toggle_maximize_stream_window([this] {
if (!ui_ || !ui_->stream) {
return;
}
auto& stream = *ui_->stream;
auto& window = stream->window();
const bool maximize = !window.is_maximized();
stream->set_window_maximized(maximize);
window.set_maximized(maximize);
});
stream->on_close_stream_window(close_stream_sessions);
stream->on_select_tab([this](int index) { SelectStreamTab(index); });
stream->on_reorder_tab([this](int from, float drop_x, float tab_width) {
ReorderStreamTab(from, drop_x, tab_width);
});
stream->on_close_tab(
[this](slint::SharedString id) { CloseStreamTab(std::string(id)); });
stream->on_switch_display([this](int index) {
auto props = SelectedSession();
if (!props || index < 0 ||
index >= static_cast<int>(props->display_info_list_.size())) {
return;
}
props->selected_display_ = index;
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->cursor_presentation_ = {};
}
RemoteAction action{};
action.type = ControlType::display_id;
action.d = index;
const std::string message = action.to_json();
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->control_data_label_.c_str());
});
stream->on_send_shortcut([this](slint::SharedString shortcut) {
auto props = SelectedSession();
if (!props || !props->peer_) {
return;
}
RemoteAction action{};
action.type = ControlType::service_command;
action.c.flag = std::string(shortcut) == "Ctrl+Alt+Del"
? ServiceCommandFlag::send_sas
: ServiceCommandFlag::lock_workstation;
const std::string message = action.to_json();
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->control_data_label_.c_str());
});
stream->on_toggle_privacy([this] {
auto props = SelectedSession();
if (!props || !props->peer_ || !props->control_mouse_ ||
props->connection_status_.load() != ConnectionStatus::Connected) return;
RemoteAction action{};
action.type = ControlType::privacy_command;
{
std::lock_guard lock(props->privacy_status_mutex_);
const auto& status = props->privacy_status_;
const uint64_t now = SDL_GetTicks();
const auto controls = GetPrivacyControlState(
status, props->privacy_status_received_, props->privacy_command_pending_,
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
if (!controls.can_toggle) return;
action.pc.flag = controls.active ? PrivacyCommandFlag::disable
: PrivacyCommandFlag::enable;
action.pc.block_local_input = true;
props->privacy_command_pending_ = true;
props->privacy_command_tick_ = now;
props->privacy_request_revision_ = status.revision;
}
const auto message = action.to_json();
if (SendReliableDataFrame(props->peer_, message.data(), message.size(),
props->control_data_label_.c_str()) != 0) {
std::lock_guard lock(props->privacy_status_mutex_);
props->privacy_command_pending_ = false;
props->privacy_status_received_ = false;
LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_);
}
});
stream->on_toggle_mouse_control([this] {
auto props = SelectedSession();
if (!props || !props->connection_established_) {
return;
}
props->control_mouse_ = !props->control_mouse_;
props->enable_mouse_control_ = props->control_mouse_;
start_keyboard_capturer_ = props->control_mouse_;
if (!props->control_mouse_) {
devices_.StopKeyboardCapturer();
keyboard_.ForceReleasePressedKeys();
}
});
stream->on_change_video_settings([this](int quality, int frame_rate_index,
int preference) {
auto props = SelectedSession();
if (!props || !props->peer_ ||
props->connection_status_.load() != ConnectionStatus::Connected)
return;
RemoteAction action{};
action.type = ControlType::video_settings;
action.vs = {quality, frame_rate_index == 0 ? 30 : 60, preference};
if (frame_rate_index < 0 || frame_rate_index > 1 ||
!ValidVideoSettings(action.vs))
return;
{
std::lock_guard lock(props->video_settings_mutex_);
if (!props->video_settings_supported_) return;
action.vs.request_id = props->video_settings_.request_id + 1;
const auto message = action.to_json();
if (SendReliableDataFrame(props->peer_, message.data(), message.size(),
props->control_data_label_.c_str()) != 0)
return;
props->video_settings_ = action.vs;
props->video_settings_pending_ = true;
props->video_settings_failed_ = false;
props->video_settings_request_tick_ = SDL_GetTicks();
}
});
stream->on_toggle_audio([this] {
auto props = SelectedSession();
if (!props || !props->connection_established_) {
return;
}
props->audio_capture_button_pressed_ =
!props->audio_capture_button_pressed_;
RemoteAction action{};
action.type = ControlType::audio_capture;
action.a = props->audio_capture_button_pressed_;
const std::string message = action.to_json();
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->control_data_label_.c_str());
});
stream->on_select_file([this] {
auto props = SelectedSession();
if (!props) {
return;
}
transfers_.ProcessSelectedFile(
OpenFileDialog(localization::select_file[localization_language_index_]),
props, props->file_label_);
});
stream->on_close_file_transfer([this] {
if (auto props = SelectedSession()) {
props->file_transfer_.file_transfer_window_visible_ = false;
}
});
stream->on_can_drop_file(
[](const slint::DataTransfer& data) { return data.has_plain_text(); });
stream->on_file_dropped([this](const slint::DataTransfer& data) {
const auto text = data.plain_text();
auto props = SelectedSession();
if (!text || !props) {
return;
}
const std::string path = DecodeDroppedPath(std::string(*text));
if (!path.empty()) {
transfers_.ProcessSelectedFile(path, props, props->file_label_,
props->remote_id_);
}
});
stream->on_toggle_stats([this] {
if (auto props = SelectedSession()) {
props->net_traffic_stats_button_pressed_ =
!props->net_traffic_stats_button_pressed_;
}
});
stream->on_toggle_fullscreen([this] {
auto& stream = *ui_->stream;
#if defined(__APPLE__)
const bool enter_fullscreen = !IsStreamWindowFullscreen();
// Update Slint's fullscreen layout before AppKit applies the new frame so
// the first fullscreen draw cannot briefly contain the windowed tab strip.
stream->set_fullscreen_enabled(enter_fullscreen);
if (!SetStreamWindowFullscreen(enter_fullscreen)) {
stream->set_fullscreen_enabled(!enter_fullscreen);
return;
}
#else
auto& window = stream->window();
const bool enter_fullscreen = !window.is_fullscreen();
#endif
fullscreen_button_pressed_ = enter_fullscreen;
#if defined(__APPLE__)
video_frame_dirty_.store(true, std::memory_order_release);
#else
stream->set_fullscreen_enabled(enter_fullscreen);
window.request_redraw();
window.set_fullscreen(enter_fullscreen);
#endif
});
stream->on_disconnect([this] {
if (auto props = SelectedSession()) {
CloseStreamTab(props->remote_id_);
}
});
stream->on_pointer_input([this](slint::PointerEventButton button,
slint::PointerEventKind kind, float x,
float y) {
SendPointerInput(static_cast<int>(button), static_cast<int>(kind), x, y);
});
stream->on_scroll_input([this](float dx, float dy, float x, float y) {
SendScrollInput(dx, dy, x, y);
});
stream->on_key_input([this](slint::SharedString text, bool pressed,
bool control, bool alt, bool shift, bool meta) {
SendKeyInput(std::string(text), pressed, control, alt, shift, meta);
});
stream->on_keyboard_focus_changed(
[this](bool focused) { SetStreamKeyboardFocus(focused); });
}
void GuiApplication::BindServerCallbacks() {
if (!ui_->server) {
return;
}
auto& server = *ui_->server;
auto& bar = *ui_->server_bar;
bar->on_title_drag([this](int phase, float, float) {
auto& drag = ui_->server_drag;
if (phase == 1) {
ui_->server_drag_moved = false;
(*ui_->server_bar)->set_dragged(false);
drag.active = CanUseGlobalPointerPosition() &&
drag.ReadGlobalPointer(&drag.pointer_start_x, &drag.pointer_start_y);
ui_->server_drag_anchor = ui_->server_anchor;
} else if (phase == 0) {
PositionServerWindows();
drag.active = false;
}
});
bar->on_toggle_panel([this] {
SetServerPanelExpanded(server_window_collapsed_);
});
bar->on_move_anchor([this](float delta) {
ui_->server_anchor = std::clamp(ui_->server_anchor + delta, 0.0f, 1.0f);
PositionServerWindows();
});
const auto dismiss = [this] { SetServerPanelExpanded(false); };
bar->on_dismiss_panel(dismiss);
server->on_dismiss_panel(dismiss);
server->window().on_close_requested([dismiss] {
dismiss();
return slint::CloseRequestResponse::KeepWindowShown;
});
bar->window().on_close_requested([dismiss] {
dismiss();
return slint::CloseRequestResponse::KeepWindowShown;
});
server->on_controller_selected([this](int index) {
if (index < 0 || index >= static_cast<int>(ui_->controller_ids.size())) {
return;
}
selected_server_remote_id_ = ui_->controller_ids[index];
});
server->on_select_file([this] {
if (selected_server_remote_id_.empty() || ui_->server_file_dialog_open) {
return;
}
// A native file picker runs a nested event loop. Keep the recipient stable
// if another controller connects or leaves while it is open.
const std::string recipient = selected_server_remote_id_;
ui_->server_file_dialog_open = true;
(*ui_->server)->window().request_redraw();
// TouchArea clears pressed only after its clicked callback returns. Allow
// that release and a repaint before entering the blocking native picker.
ui_->server_file_dialog_timer.start(
slint::TimerMode::SingleShot, std::chrono::milliseconds(32),
[this, recipient] {
const auto connected = [this, &recipient] {
std::shared_lock lock(connection_status_mutex_);
const auto found = connection_status_.find(recipient);
return found != connection_status_.end() &&
found->second == ConnectionStatus::Connected;
};
if (!connected()) {
ui_->server_file_dialog_open = false;
return;
}
const auto path = OpenFileDialog(
localization::select_file[localization_language_index_]);
ui_->server_file_dialog_open = false;
if (connected() && !path.empty())
transfers_.ProcessSelectedFile(path, nullptr, file_label_, recipient);
});
});
server->on_disconnect_controller([this](slint::SharedString id) {
CloseServerController(std::string(id));
});
server->on_disconnect_all([this] {
// Work from the confirmed UI snapshot; a later arrival is a new session.
const auto ids = ui_->controller_ids;
for (const auto& id : ids) CloseServerController(id);
});
}
void GuiApplication::Tick() {
if (exit_) {
slint::quit_event_loop();
return;
}
PollUpdateCheck();
HandleSessionCleanup();
HandlePasswordChangeResult();
HandleCredentialRecovery();
if (!peer_) {
CreateConnectionPeer();
}
#ifdef __linux__
HeadlessConsole::Instance().Poll(
signal_connected_.load(),
[this](const std::string& password) { RequestPasswordChange(password); },
[this] {
const auto password = GenerateRandomPassword(password_saved_);
if (password) RequestPasswordChange(*password);
else HeadlessConsole::Instance().NotifyKey("console_random_failed");
},
[this] { exit_ = true; });
#endif
SDL_Event event;
#if defined(__APPLE__)
// Slint's winit backend owns the macOS AppKit event loop. SDL_PollEvent()
// implicitly pumps Cocoa events, so calling it from this Slint timer would
// re-enter winit's event handler and abort the process. Only drain events
// that are already in SDL's queue; clipboard changes are polled below and
// tray/window actions use direct Slint callbacks on macOS.
while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST,
SDL_EVENT_LAST) > 0) {
#else
while (SDL_PollEvent(&event)) {
#endif
if (event.type == SDL_EVENT_QUIT) {
exit_ = true;
} else if (event.type == SDL_EVENT_CLIPBOARD_UPDATE) {
clipboard_.HandleLocalUpdate();
}
}
clipboard_.ApplyPendingRemoteText();
#if defined(__linux__) && !defined(__APPLE__)
if (ui_->tray) {
ui_->tray->ProcessEvents();
}
#endif
const auto now = std::chrono::steady_clock::now();
if (now - ui_->last_clipboard_poll >= 250ms) {
clipboard_.HandleLocalUpdate();
ui_->last_clipboard_poll = now;
}
if (need_to_rejoin_ && now - last_rejoin_check_time_ >= 1s) {
last_rejoin_check_time_ = now;
need_to_rejoin_ = false;
for (const auto& [_, props] : remote_sessions_) {
if (props && props->rejoin_) {
ConnectTo(props->remote_id_, props->remote_password_,
props->remember_password_);
}
}
}
const bool was_reloading_recent_connections = reload_recent_connections_;
HandleRecentConnections();
if (was_reloading_recent_connections && !reload_recent_connections_) {
ui_->recent_model_signature.clear();
}
HandleConnectionStatusChange();
HandlePendingPresenceProbe();
HandlePresenceProbeTimeout();
HandlePrivacy();
HandleWindowsServiceIntegration();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#else
SyncStreamKeyboardFocus();
#endif
devices_.UpdateInteractions();
ShareLocalCursorState();
CloseRemoteClosedTabs();
UpdateLocalization();
SyncMainWindow();
SyncConnectionDialog();
SyncPlatformDialogs();
SyncStreamWindow();
SyncServerWindow();
}
void GuiApplication::PollUpdateCheck() {
// The first tick runs after show()/event-loop startup. Poll never waits for
// networking and all application/model changes remain on this UI thread.
if (!update_checker_->Poll() ||
update_checker_->status() == UpdateChecker::Status::Failed) {
return;
}
const auto& info = *update_checker_->latest();
latest_version_ = "v" + info.version;
release_name_ = info.release_name;
release_notes_ = info.release_notes;
release_date_ = info.release_date;
update_available_ = update_checker_->update_available();
std::vector<ui::ReleaseNoteBlock> blocks;
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
ui::ReleaseNoteBlock block;
block.content = parsed.content;
block.section_gap = parsed.section_gap;
blocks.push_back(std::move(block));
}
ui_->release_note_blocks_model->set_vector(std::move(blocks));
}
void GuiApplication::ShareLocalCursorState() {
constexpr auto kCursorEchoSuppressionInterval = 300ms;
constexpr auto kCursorStateHeartbeatInterval = 500ms;
constexpr auto kCursorPositionShareInterval = 16ms;
constexpr float kCursorPositionEpsilon = 0.00005f;
std::vector<std::string> connected_controllers;
{
std::shared_lock lock(connection_status_mutex_);
connected_controllers.reserve(connection_status_.size());
for (const auto& [remote_id, status] : connection_status_) {
if (status == ConnectionStatus::Connected) {
connected_controllers.push_back(remote_id);
}
}
}
if (!is_server_mode_ || !peer_ || connected_controllers.empty()) {
cursor_delivery_states_.clear();
return;
}
CursorState sampled{};
const bool sample_ok = cursor_state_provider_.Sample(
devices_.display_info_list(), selected_display_, &sampled);
if (!sample_ok) {
sampled.render_mode = CursorRenderMode::unknown;
if (sampled.hidden_reason == CursorHiddenReason::unspecified) {
sampled.hidden_reason = CursorHiddenReason::sampling_failed;
}
} else if (sampled.render_mode == CursorRenderMode::legacy) {
// Providers without platform-specific presentation metadata only need to
// sample visible/shape; normalize that common case once for every peer.
sampled.render_mode = sampled.visible ? CursorRenderMode::separate
: CursorRenderMode::hidden;
sampled.hidden_reason = sampled.visible ? CursorHiddenReason::unspecified
: CursorHiddenReason::system_hidden;
}
const auto now = std::chrono::steady_clock::now();
std::unordered_map<std::string, std::chrono::steady_clock::time_point>
last_input_times;
{
std::lock_guard lock(remote_pointer_input_mutex_);
for (const auto& remote_id : connected_controllers) {
const auto input_it = last_remote_pointer_input_time_.find(remote_id);
if (input_it != last_remote_pointer_input_time_.end()) {
last_input_times.emplace(remote_id, input_it->second);
}
}
}
std::unordered_set<std::string> connected_ids(connected_controllers.begin(),
connected_controllers.end());
std::erase_if(cursor_delivery_states_, [&](const auto& entry) {
return connected_ids.find(entry.first) == connected_ids.end();
});
struct CursorRecipient {
std::string remote_id;
bool include_position = true;
};
std::vector<CursorRecipient> recipients;
for (const auto& remote_id : connected_controllers) {
auto& delivery = cursor_delivery_states_[remote_id];
const auto input_it = last_input_times.find(remote_id);
const bool suppress_position =
input_it != last_input_times.end() &&
now - input_it->second < kCursorEchoSuppressionInterval;
delivery.feedback_pending =
delivery.feedback_pending || suppress_position;
const CursorState& previous = delivery.last_sent;
const bool shape_changed =
!delivery.has_sent || sampled.render_mode != previous.render_mode ||
sampled.hidden_reason != previous.hidden_reason ||
(sample_ok &&
(sampled.visible != previous.visible || sampled.shape != previous.shape ||
std::abs(sampled.visual_offset_x - previous.visual_offset_x) >
kCursorPositionEpsilon ||
std::abs(sampled.visual_offset_y - previous.visual_offset_y) >
kCursorPositionEpsilon));
// Preserve sub-point cursor motion. At 10x client zoom, the old roughly
// one-logical-pixel threshold became several visible phone points.
const bool position_changed =
sample_ok &&
(!delivery.has_sent || sampled.position_valid != previous.position_valid ||
sampled.display_id != previous.display_id ||
(sampled.position_valid && previous.position_valid &&
(std::abs(sampled.x - previous.x) > kCursorPositionEpsilon ||
std::abs(sampled.y - previous.y) > kCursorPositionEpsilon)));
const bool position_update_due =
position_changed &&
(delivery.last_sent_time.time_since_epoch().count() == 0 ||
now - delivery.last_sent_time >= kCursorPositionShareInterval);
const bool heartbeat_due =
delivery.last_sent_time.time_since_epoch().count() == 0 ||
now - delivery.last_sent_time >= kCursorStateHeartbeatInterval;
// Position feedback waits for a good sample and the echo-suppression
// interval. Appearance changes and heartbeats are independent of both.
const bool include_position = sample_ok && !suppress_position;
if (shape_changed || heartbeat_due ||
(include_position && (delivery.feedback_pending || position_update_due))) {
recipients.push_back({remote_id, include_position});
}
}
if (recipients.empty()) return;
sampled.seq = ++cursor_state_sequence_;
for (const auto& recipient : recipients) {
auto& delivery = cursor_delivery_states_[recipient.remote_id];
CursorState outgoing = sampled;
if (!sample_ok) {
// Explicitly report unknown to new receivers, but retain the last
// presentation for peers that only understand visible/shape.
outgoing = delivery.has_sent ? delivery.last_sent : CursorState{};
if (!delivery.has_sent) {
outgoing.visible = true;
outgoing.shape = RemoteCursorShape::default_cursor;
}
outgoing.seq = sampled.seq;
outgoing.render_mode = sampled.render_mode;
outgoing.hidden_reason = sampled.hidden_reason;
}
outgoing.position_update = recipient.include_position;
if (!recipient.include_position) {
// Keep legacy receivers at their last acknowledged position as well.
// New receivers honor position_update=false and leave their optimistic
// local position untouched.
outgoing.position_valid =
delivery.has_sent && delivery.last_sent.position_valid;
outgoing.x = delivery.has_sent ? delivery.last_sent.x : 0.5f;
outgoing.y = delivery.has_sent ? delivery.last_sent.y : 0.5f;
outgoing.display_id = delivery.has_sent
? delivery.last_sent.display_id
: -1;
}
RemoteAction action{};
action.type = ControlType::cursor_state;
action.cs = outgoing;
const std::string message = action.to_json();
const int result = SendReliableDataFrameToPeer(
peer_, message.c_str(), message.size(), mouse_label_.c_str(),
recipient.remote_id.c_str(), recipient.remote_id.size());
if (result != 0) {
LOG_WARN("Send cursor state to [{}] failed, ret={}",
recipient.remote_id, result);
continue;
}
if (!delivery.has_sent ||
delivery.last_sent.render_mode != outgoing.render_mode ||
delivery.last_sent.hidden_reason != outgoing.hidden_reason) {
LOG_DEBUG("Send cursor presentation to [{}]: mode={}, reason={}",
recipient.remote_id, static_cast<int>(outgoing.render_mode),
static_cast<int>(outgoing.hidden_reason));
}
delivery.last_sent = outgoing;
delivery.has_sent = true;
if (recipient.include_position) {
delivery.feedback_pending = false;
}
delivery.last_sent_time = now;
}
}
bool GuiApplication::RequestPasswordChange(const std::string &password) {
const auto notify = [](const std::string& message) {
#ifdef __linux__
HeadlessConsole::Instance().NotifyKey(message.c_str());
#else
(void)message;
#endif
};
if (password.size() != 6 ||
password.find_first_not_of("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") !=
std::string::npos) {
notify("console_password_invalid");
return false;
}
if (!peer_ || !signal_connected_) {
offline_warning_text_ =
localization::signal_disconnected[localization_language_index_];
show_offline_warning_window_ = true;
notify("console_password_offline");
return true;
}
std::string request_id;
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
if (password_change_pending_) {
notify("console_password_pending");
return true;
}
request_id =
CreatePasswordChangeRequestId(++next_password_change_request_id_);
password_change_pending_ = true;
password_change_result_ready_ = false;
password_change_succeeded_ = false;
password_change_result_uncertain_ = false;
password_change_requested_at_ = std::chrono::steady_clock::now();
pending_password_change_request_id_ = request_id;
pending_local_password_ = password;
password_change_error_.clear();
}
const bool self_hosted = config_center_->IsSelfHosted();
const std::string server_host =
self_hosted ? config_center_->GetSignalServerHost()
: config_center_->GetDefaultServerHost();
const int server_port =
self_hosted ? config_center_->GetSignalServerPort()
: config_center_->GetDefaultSignalServerPort();
const std::string pending_identity = std::string(client_id_) + "@" + password;
if (!settings_.StagePendingPasswordChange(
pending_identity, self_hosted, server_host, server_port, request_id)) {
std::lock_guard<std::mutex> lock(password_change_mutex_);
password_change_pending_ = false;
pending_password_change_request_id_.clear();
pending_local_password_.clear();
password_change_error_.clear();
offline_warning_text_ = localization::failed[localization_language_index_];
show_offline_warning_window_ = true;
LOG_ERROR("Could not durably stage password change");
notify("console_password_stage_failed");
return true;
}
const nlohmann::json request = {{"type", "change_password"},
{"request_id", request_id},
{"new_password", password}};
const std::string message = request.dump();
if (SendSignalMessage(peer_, message.data(), message.size()) != 0) {
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
password_change_pending_ = false;
pending_password_change_request_id_.clear();
pending_local_password_.clear();
offline_warning_text_ =
localization::signal_disconnected[localization_language_index_];
show_offline_warning_window_ = true;
}
if (!settings_.ClearPendingPasswordChange()) {
LOG_WARN("Could not clear unsent pending password change");
}
notify("console_password_send_failed");
} else {
notify("console_password_submitted");
}
return true;
}
void GuiApplication::HandlePasswordChangeResult() {
bool succeeded = false;
bool uncertain = false;
std::string error;
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
if (password_change_pending_ && !password_change_result_ready_ &&
std::chrono::steady_clock::now() - password_change_requested_at_ >=
std::chrono::seconds(10)) {
password_change_result_ready_ = true;
password_change_succeeded_ = false;
password_change_result_uncertain_ = true;
password_change_error_ = "Server did not respond";
}
if (!password_change_result_ready_) {
return;
}
succeeded = password_change_succeeded_;
uncertain = password_change_result_uncertain_;
error = password_change_error_;
password_change_pending_ = false;
password_change_result_ready_ = false;
password_change_succeeded_ = false;
password_change_result_uncertain_ = false;
pending_password_change_request_id_.clear();
pending_local_password_.clear();
password_change_error_.clear();
}
if (!succeeded) {
#ifdef __linux__
HeadlessConsole::Instance().NotifyKey(
uncertain ? "console_password_uncertain" : "console_password_rejected");
#endif
if (uncertain) {
LOG_WARN("Password change outcome is unknown: {}; re-authenticating",
error);
} else {
LOG_WARN("Password change failed: {}", error);
if (!settings_.ClearPendingPasswordChange()) {
LOG_WARN("Could not clear rejected pending password change");
}
}
offline_warning_text_ = localization::failed[localization_language_index_];
if (!error.empty()) {
offline_warning_text_ += ": " + error;
}
show_offline_warning_window_ = true;
if (uncertain && peer_) {
CloseConnectionPeer();
}
return;
}
if (!settings_.PromotePendingPasswordChange()) {
// The pending credential was persisted before the request was sent, so it
// remains sufficient for recovery even when promotion cannot be written.
LOG_WARN("Password changed on server; pending recovery record retained");
}
LOG_INFO("Password changed successfully for [{}]", client_id_);
#ifdef __linux__
HeadlessConsole::Instance().NotifyKey("console_password_confirmed");
#endif
if (peer_) {
CloseConnectionPeer();
}
}
void GuiApplication::HandleCredentialRecovery() {
bool retry_active = false;
bool promote_pending = false;
bool clear_pending = false;
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
retry_active = credential_recovery_retry_active_;
promote_pending = credential_recovery_promote_pending_;
clear_pending = credential_recovery_clear_pending_;
credential_recovery_retry_active_ = false;
credential_recovery_promote_pending_ = false;
credential_recovery_clear_pending_ = false;
if (retry_active) {
credential_recovery_attempt_pending_ = false;
}
}
if (retry_active) {
LOG_INFO("Pending credential was not accepted; retrying active credential");
if (peer_) {
CloseConnectionPeer();
}
return;
}
if (promote_pending) {
#ifdef __linux__
HeadlessConsole::Instance().NotifyKey("console_password_recovered");
#endif
if (!settings_.PromotePendingPasswordChange()) {
LOG_WARN("Recovered credential is active but promotion remains pending");
} else {
LOG_INFO("Recovered password change with pending credential");
}
} else if (clear_pending) {
#ifdef __linux__
HeadlessConsole::Instance().NotifyKey("console_password_retained");
#endif
if (!settings_.ClearPendingPasswordChange()) {
LOG_WARN("Active credential recovered but pending record could not be "
"cleared");
} else {
LOG_INFO("Password change was not committed; retained active credential");
}
} else {
return;
}
std::lock_guard<std::mutex> lock(password_change_mutex_);
credential_recovery_in_progress_ = false;
credential_recovery_attempt_pending_ = false;
}
void GuiApplication::UpdateLocalization() {
const int language =
localization::detail::ClampLanguageIndex(localization_language_index_);
if (!ui_ || ui_->localized_language == language) {
return;
}
ui_localization::ApplyMainWindowStrings(ui_->main, language);
ui_localization::ApplyStreamWindowStrings(ui_->main, language);
if (ui_->stream) {
ui_localization::ApplyStreamWindowStrings(*ui_->stream, language);
}
ui_->localized_language = language;
}
void GuiApplication::SyncAnnouncements() {
if (signal_connected_) {
announcements_.Configure(params_.signal_server_ip,
params_.signal_server_port, client_id_);
}
const auto request = announcements_.TakePendingRequest();
if (!request.is_null()) {
const auto payload = request.dump();
if (!peer_ || SendSignalMessage(peer_, payload.data(), payload.size()) != 0)
announcements_.Failed(request["request_id"]);
}
if (ui_->announcement_version == announcements_.Version()) return;
const auto state = announcements_.Get();
ui_->announcement_version = state.version;
ui_->main->set_announcement_unread(state.unread);
ui_->main->set_announcement_total(state.total);
ui_->main->set_announcement_loading(state.loading);
ui_->main->set_announcement_failed(state.failed);
ui_->main->set_announcement_read_save_failed(state.read_save_failed);
ui_->main->set_announcement_delete_failed(state.delete_failed);
ui_->main->set_announcement_loaded(state.loaded);
std::vector<ui::AnnouncementItem> rows;
bool selected_found = false;
for (const auto& item : state.items) {
ui::AnnouncementItem row;
const auto id = std::to_string(item["id"].get<int64_t>());
const auto revision = std::to_string(item["revision"].get<int64_t>());
row.id = UiText(id);
row.revision = UiText(revision);
row.title = UiText(item["title"].get<std::string>());
const auto timestamp =
static_cast<time_t>(item["updated_at"].get<int64_t>());
std::tm local{};
#ifdef _WIN32
localtime_s(&local, &timestamp);
#else
localtime_r(&timestamp, &local);
#endif
char date[32]{};
std::strftime(date, sizeof(date), "%Y-%m-%d %H:%M", &local);
row.date = UiText(date);
row.read = item["read"].get<bool>();
if (id == ui_->announcement_selected_id &&
revision == ui_->announcement_selected_revision) {
selected_found = true;
ui_->main->set_announcement_selected_id(row.id);
ui_->main->set_announcement_selected_title(row.title);
ui_->main->set_announcement_selected_body(
announcement_text::Format(item["body"].get<std::string>()));
ui_->main->set_announcement_selected_date(row.date);
ui_->main->set_announcement_selected_read(row.read);
}
rows.push_back(std::move(row));
}
if (!selected_found) {
ui_->announcement_selected_id.clear();
ui_->main->set_announcement_selected_id("");
ui_->main->set_announcement_selected_body(slint::StyledText{});
}
ui_->main->set_announcements(
std::make_shared<slint::VectorModel<ui::AnnouncementItem>>(
std::move(rows)));
}
void GuiApplication::SyncMainWindow() {
if (!ui_) {
return;
}
SyncAnnouncements();
#if _WIN32
ConfigureWindowsWindowIcons(ui_->main->window().win32_hwnd());
#endif
#if defined(__APPLE__)
if (ui_->main_native_titlebar_attempts > 0) {
if (ConfigureMainWindowChrome(ui_->main->window().appkit_view())) {
ui_->main_native_titlebar_attempts = 0;
} else {
--ui_->main_native_titlebar_attempts;
}
}
#endif
if (re_enter_remote_id_) {
ui_->main->invoke_reset_remote_id();
re_enter_remote_id_ = false;
}
ui_->main->set_local_id(UiText(FormatPeerId(client_id_)));
ui_->main->set_local_password(password_saved_);
ui_->main->set_password_visible(show_password_);
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
ui_->main->set_password_change_pending(password_change_pending_);
}
const bool signal_online = signal_connected_.load();
if (signal_online) {
signal_lost_at_valid_ = false;
} else if (!signal_lost_at_valid_) {
signal_lost_at_ = std::chrono::steady_clock::now();
signal_lost_at_valid_ = true;
}
// Reconnects routinely take several seconds on a lossy path; only report
// offline once the connection has stayed down long enough to be a real
// outage rather than a blip the reconnect is already recovering from.
const bool signal_offline =
!signal_online && signal_lost_at_valid_ &&
std::chrono::steady_clock::now() - signal_lost_at_ >
kSignalOfflineGrace;
ui_->main->set_signal_connected(!signal_offline);
ui_->main->set_signal_tls_error(signal_status_ ==
SignalStatus::SignalTlsCertError);
ui_->main->set_update_available(update_available_);
switch (update_checker_->status()) {
case UpdateChecker::Status::Idle:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Idle);
break;
case UpdateChecker::Status::Checking:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Checking);
break;
case UpdateChecker::Status::UpToDate:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::UpToDate);
break;
case UpdateChecker::Status::Available:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Available);
break;
case UpdateChecker::Status::Failed:
ui_->main->set_update_check_status(ui::UpdateCheckStatus::Failed);
break;
}
ui_->main->set_current_version(CROSSDESK_VERSION);
ui_->main->set_latest_version(UiText(latest_version_));
ui_->main->set_release_name(UiText(release_name_));
ui_->main->set_release_date(UiText(release_date_));
ui_->main->set_settings_session_active(HasActiveSession());
if (show_offline_warning_window_) {
ui_->main->set_offline_warning(UiText(offline_warning_text_));
show_offline_warning_window_ = false;
}
std::ostringstream signature_builder;
signature_builder << config_center_->IsSaveRemotePreviews() << '\n';
for (const auto& [key, connection] : recent_connections_) {
signature_builder << key << '\0' << connection.remote_id << '\0'
<< connection.remote_host_name << '\0'
<< static_cast<int>(connection.platform) << '\0'
<< settings_.RecentConnectionDisplayName(connection)
<< '\0'
<< device_presence_cache_.IsOnline(connection.remote_id)
<< '\n';
}
const std::string signature = signature_builder.str();
if (ui_->recent_model_signature == signature) {
return;
}
ui_->recent_model_signature = signature;
std::vector<ui::RecentConnection> model;
model.reserve(recent_connections_.size());
for (const auto& [_, connection] : recent_connections_) {
// Slint-generated scalar fields have no defaults. With no image,
// has_thumbnail must remain false so the remote platform is rendered.
ui::RecentConnection item{};
item.remote_id = UiText(connection.remote_id);
item.host_name = UiText(connection.remote_host_name);
item.display_name =
UiText(settings_.RecentConnectionDisplayName(connection));
item.online = device_presence_cache_.IsOnline(connection.remote_id);
item.platform = UiText(HostPlatformName(connection.platform));
std::vector<unsigned char> rgba;
int image_width = 0;
int image_height = 0;
if (config_center_->IsSaveRemotePreviews() &&
!connection.image_path.empty() &&
thumbnail_->DecodeImage(connection.image_path, &rgba, &image_width,
&image_height)) {
slint::SharedPixelBuffer<slint::Rgba8Pixel> pixels(image_width,
image_height);
std::memcpy(pixels.begin(), rgba.data(), rgba.size());
item.thumbnail = slint::Image(std::move(pixels));
item.has_thumbnail = true;
}
model.push_back(std::move(item));
}
// LoadThumbnail already orders connections by last update time. Keep that
// order within each presence group while placing online devices first.
std::stable_partition(
model.begin(), model.end(),
[](const ui::RecentConnection& connection) { return connection.online; });
ui_->recent_model->set_vector(std::move(model));
}
void GuiApplication::SyncConnectionDialog() {
if (!ui_ || !show_connection_status_window_) {
if (ui_) {
ui_->main->set_connection_dialog_open(false);
ui_->connection_password_initialized = false;
}
return;
}
auto props = FindRemoteSession(focused_remote_id_);
if (!props) {
ui_->main->set_connection_dialog_open(false);
return;
}
if (ui_->connection_dialog_remote_id != props->remote_id_) {
ui_->connection_password_initialized = false;
}
ui_->connection_dialog_remote_id = props->remote_id_;
const ConnectionStatus status = props->connection_status_.load();
const auto auth = props->connection_auth_.Get();
const bool password_required = auth.password_required ||
(status == ConnectionStatus::IncorrectPassword && auth.validating);
const bool pending = !password_required &&
(status == ConnectionStatus::Connecting || status == ConnectionStatus::Gathering);
std::string text;
if (password_required) {
text = auth.validating
? localization::validate_password[localization_language_index_]
: auth.incorrect_password
? localization::reinput_password[localization_language_index_]
: localization::input_password[localization_language_index_];
} else if (auth.error == minirtc::JoinFailureKind::Throttled) {
text = localization::connection_auth_throttled[localization_language_index_];
} else if (auth.error == minirtc::JoinFailureKind::Busy) {
text = localization::connection_service_busy[localization_language_index_];
} else if (auth.error == minirtc::JoinFailureKind::NotAuthenticated) {
text = localization::connection_not_authenticated[localization_language_index_];
} else {
text = ConnectionStatusText(status, localization_language_index_);
}
if (auth.retry_after > 0) {
char retry_text[256];
std::snprintf(retry_text, sizeof(retry_text),
localization::connection_retry_after[localization_language_index_].c_str(),
auth.retry_after);
text += "\n" + std::string(retry_text);
}
ui_->main->set_connection_dialog_open(true);
ui_->main->set_connection_status_text(UiText(text));
ui_->main->set_connection_pending(pending);
ui_->main->set_connection_password_required(password_required);
ui_->main->set_connection_validating(auth.validating);
if (password_required && !auth.validating &&
!ui_->connection_password_initialized) {
ui_->main->set_connection_password(props->remote_password_);
ui_->main->set_connection_remember_password(props->remember_password_);
ui_->connection_password_initialized = true;
} else if (!password_required) {
ui_->connection_password_initialized = false;
}
}
void GuiApplication::SyncPlatformDialogs() {
#if _WIN32 && CROSSDESK_PORTABLE
CheckPortableWindowsService();
const PortableServiceInstallState state =
portable_service_install_state_.load(std::memory_order_acquire);
if (state == PortableServiceInstallState::succeeded) {
portable_service_installed_ = true;
}
ui_->main->set_portable_service_settings_visible(true);
ui_->main->set_portable_service_dialog_open(
show_portable_service_install_window_);
ui_->main->set_portable_service_suppressed_notice_open(
show_portable_service_prompt_suppressed_window_);
ui_->main->set_portable_service_installed(portable_service_installed_);
ui_->main->set_portable_service_installing(
state == PortableServiceInstallState::installing);
ui_->main->set_portable_service_succeeded(
state == PortableServiceInstallState::succeeded);
const std::string* status = nullptr;
if (state == PortableServiceInstallState::installing) {
status =
&localization::installing_windows_service[localization_language_index_];
} else if (state == PortableServiceInstallState::succeeded) {
status = &localization::windows_service_install_success
[localization_language_index_];
} else if (state == PortableServiceInstallState::failed) {
status = &localization::windows_service_install_failed
[localization_language_index_];
}
ui_->main->set_portable_service_status(status ? UiText(*status)
: slint::SharedString{});
if (show_portable_service_install_window_) {
if (!ui_->portable_service_dialog_initialized) {
ui_->main->set_portable_service_do_not_remind(
portable_service_do_not_remind_);
ui_->portable_service_dialog_initialized = true;
}
} else {
ui_->portable_service_dialog_initialized = false;
}
#else
ui_->main->set_portable_service_settings_visible(false);
ui_->main->set_portable_service_dialog_open(false);
ui_->main->set_portable_service_suppressed_notice_open(false);
#endif
#ifdef __APPLE__
RefreshMacPermissionStatus(false);
show_request_permission_window_ = !mac_screen_recording_permission_granted_ ||
!mac_accessibility_permission_granted_;
ui_->main->set_permission_dialog_open(show_request_permission_window_);
ui_->main->set_screen_recording_granted(
mac_screen_recording_permission_granted_);
ui_->main->set_accessibility_granted(mac_accessibility_permission_granted_);
#else
ui_->main->set_permission_dialog_open(false);
#endif
}
#if defined(__linux__) && !defined(__APPLE__)
void GuiApplication::SyncXWaylandWindowActivation() {
if (!ui_ || !use_xwayland_gui_) {
return;
}
const auto now = std::chrono::steady_clock::now();
if (now - ui_->last_x11_activation_poll < 100ms) {
return;
}
ui_->last_x11_activation_poll = now;
if (!ui_->x11_focus_display) {
ui_->x11_focus_display = XOpenDisplay(nullptr);
}
Display* display = ui_->x11_focus_display;
const ::Window active_window = X11GetActiveWindow(display);
if (!active_window) {
SetStreamKeyboardFocus(false);
return;
}
if (const auto process_id = X11GetWindowProcessId(display, active_window);
process_id && *process_id != static_cast<unsigned long>(getpid())) {
ui_->main->set_window_active(false);
if (ui_->stream) {
(*ui_->stream)->set_window_active(false);
}
SetStreamKeyboardFocus(false);
return;
}
ui_->main->set_window_active(
X11WindowMatches(display, active_window, ui_->main->window()));
if (ui_->stream) {
const bool stream_active =
X11WindowMatches(display, active_window, (*ui_->stream)->window());
(*ui_->stream)->set_window_active(stream_active);
SetStreamKeyboardFocus(stream_active);
} else {
SetStreamKeyboardFocus(false);
}
}
#endif
void GuiApplication::SetStreamKeyboardFocus(bool focused) {
// The local upgrade dialog owns input even while the native window is active.
if (focused && ui_ && ui_->stream &&
(*ui_->stream)->get_remote_update_visible()) {
focused = false;
}
if (focus_on_stream_window_ == focused) {
return;
}
focus_on_stream_window_ = focused;
if (!focused) {
devices_.StopKeyboardCapturer();
keyboard_.ForceReleasePressedKeys();
}
}
void GuiApplication::SyncStreamKeyboardFocus() {
if (!ui_ || !ui_->stream || !(*ui_->stream)->window().is_visible()) {
SetStreamKeyboardFocus(false);
return;
}
#if _WIN32
const HWND stream_hwnd = (*ui_->stream)->window().win32_hwnd();
SetStreamKeyboardFocus(stream_hwnd != nullptr &&
GetForegroundWindow() == stream_hwnd);
#elif defined(__APPLE__)
SetStreamKeyboardFocus(IsStreamWindowActive());
#endif
}
void GuiApplication::SyncStreamWindow() {
bool has_sessions = false;
{
std::shared_lock lock(remote_sessions_mutex_);
has_sessions = !remote_sessions_.empty();
}
if (!has_sessions) need_to_create_stream_window_ = false;
if (need_to_create_stream_window_ && !ui_->stream) {
ui_->stream.emplace(ui::StreamWindow::create());
#if defined(__linux__)
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
#endif
RegisterFontAwesome((*ui_->stream)->window());
ConfigureStreamVideoRenderer();
// Slint globals belong to a component tree. Force the next localization
// pass to initialize the newly-created, independent stream window tree.
ui_->localized_language = -1;
(*ui_->stream)->set_tabs(ui_->tab_model);
(*ui_->stream)->set_displays(ui_->display_model);
(*ui_->stream)->set_file_transfers(ui_->transfer_model);
(*ui_->stream)->set_stats_rows(ui_->stats_model);
BindStreamCallbacks();
(*ui_->stream)->show();
// Repeat after the next layout pass because the backend resolves the
// initial DPI and preferred size asynchronously on some platforms.
ui_->stream_initial_position_attempts = 2;
#if defined(__APPLE__)
ui_->stream_live_resize_configuration_attempts = 30;
#endif
stream_window_created_ = true;
stream_window_inited_ = true;
need_to_create_stream_window_ = false;
}
if (!ui_->stream) {
return;
}
#if _WIN32
ConfigureWindowsWindowIcons((*ui_->stream)->window().win32_hwnd());
#endif
(*ui_->stream)->set_window_maximized((*ui_->stream)->window().is_maximized());
#if defined(__APPLE__)
if (ui_->stream_live_resize_configuration_attempts > 0) {
if (ConfigureStreamWindowLiveResize(
(*ui_->stream)->window().appkit_view())) {
ui_->stream_live_resize_configuration_attempts = 0;
} else {
--ui_->stream_live_resize_configuration_attempts;
}
}
#endif
if (ui_->video_presenter && ui_->video_presenter->EnsureAttached()) {
video_frame_dirty_.store(true, std::memory_order_release);
}
if (ui_->stream_initial_position_attempts > 0) {
if (PositionWindowAtCenter(
(*ui_->stream)->window(), ui_->main->window(),
kStreamWindowLogicalWidth,
StreamWindowLogicalHeight((*ui_->stream)->get_custom_titlebar()))) {
--ui_->stream_initial_position_attempts;
} else {
ui_->stream_initial_position_attempts = 0;
}
}
if (!has_sessions) {
SetStreamKeyboardFocus(false);
#if defined(__APPLE__)
void* stream_view = (*ui_->stream)->window().appkit_view();
SetStreamWindowFullscreen(false);
UnregisterStreamWindow(stream_view);
#endif
if (ui_->video_presenter) {
ui_->video_presenter->Detach();
}
(*ui_->stream)->hide();
ui_->stream.reset();
stream_window_created_ = false;
stream_window_inited_ = false;
ui_->stream_initial_position_attempts = 0;
ui_->stream_live_resize_configuration_attempts = 0;
focused_remote_id_.clear();
controlled_remote_id_.clear();
ui_->tab_order.clear();
ui_->tab_ids.clear();
ui_->tab_model_signature.clear();
return;
}
std::vector<ui::StreamTab> tabs;
std::unordered_map<std::string, ui::StreamTab> tabs_by_id;
{
std::shared_lock lock(remote_sessions_mutex_);
tabs_by_id.reserve(remote_sessions_.size());
for (const auto& [id, props] : remote_sessions_) {
if (!props || !props->tab_opened_) {
continue;
}
ui::StreamTab tab;
tab.remote_id = UiText(id);
tab.title = UiText(
props->remote_host_name_.empty() ? id : props->remote_host_name_);
tab.connected =
props->connection_status_.load() == ConnectionStatus::Connected;
tabs_by_id.emplace(id, std::move(tab));
}
}
std::erase_if(ui_->tab_order, [&tabs_by_id](const std::string& id) {
return !tabs_by_id.contains(id);
});
for (const auto& [id, _] : tabs_by_id) {
if (std::find(ui_->tab_order.begin(), ui_->tab_order.end(), id) ==
ui_->tab_order.end()) {
ui_->tab_order.push_back(id);
}
}
ui_->tab_ids = ui_->tab_order;
tabs.reserve(ui_->tab_ids.size());
std::ostringstream tab_signature_builder;
for (const auto& id : ui_->tab_ids) {
if (auto found = tabs_by_id.find(id); found != tabs_by_id.end()) {
tab_signature_builder << id << '\0' << std::string(found->second.title)
<< '\0' << found->second.connected << '\n';
tabs.push_back(std::move(found->second));
}
}
const std::string tab_signature = tab_signature_builder.str();
if (ui_->tab_model_signature != tab_signature) {
ui_->tab_model_signature = tab_signature;
ui_->tab_model->set_vector(std::move(tabs));
#if defined(__APPLE__)
// Adding or removing a tab changes the native video's top inset.
video_frame_dirty_.store(true, std::memory_order_release);
#endif
}
int selected_index = 0;
if (focused_remote_id_.empty() && !ui_->tab_ids.empty()) {
focused_remote_id_ = ui_->tab_ids.front();
}
for (int i = 0; i < static_cast<int>(ui_->tab_ids.size()); ++i) {
if (ui_->tab_ids[i] == focused_remote_id_) {
selected_index = i;
break;
}
}
(*ui_->stream)->set_selected_tab(selected_index);
SelectStreamTab(selected_index);
auto props = SelectedSession();
if (!props) {
return;
}
const ConnectionStatus status = props->connection_status_.load();
bool remote_update_available = false;
if (status == ConnectionStatus::Connected) {
std::lock_guard lock(props->remote_version_mutex_);
// Wait for host_info: a version still in transit is not a legacy peer.
if (props->remote_host_info_received_ && !props->remote_update_dismissed_) {
remote_update_available = props->remote_app_version_.empty() ||
(update_checker_->latest() &&
AvailableAppUpdate(props->remote_app_version_,
*update_checker_->latest()).has_value());
}
}
(*ui_->stream)->set_remote_update_visible(remote_update_available);
if (remote_update_available) {
SetStreamKeyboardFocus(false);
}
const bool waiting_for_frame =
status == ConnectionStatus::Connected &&
!(*ui_->stream)->get_has_frame();
// Once the peer is connected, the frame-waiting hint replaces the
// connection status. Keeping these states mutually exclusive also avoids a
// transient overlap when both properties are synchronized in one tick.
(*ui_->stream)
->set_status_text(UiText(
status == ConnectionStatus::Connected
? std::string{}
: ConnectionStatusText(status, localization_language_index_)));
(*ui_->stream)
->set_receiving_text(UiText(
waiting_for_frame
? localization::receiving_screen[localization_language_index_]
: std::string{}));
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
{
std::lock_guard lock(props->privacy_status_mutex_);
const auto& status = props->privacy_status_;
const uint64_t now = SDL_GetTicks();
const auto controls = GetPrivacyControlState(
status, props->privacy_status_received_, props->privacy_command_pending_,
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
const bool connected =
props->connection_status_.load() == ConnectionStatus::Connected;
(*ui_->stream)->set_privacy_active(controls.active);
(*ui_->stream)->set_privacy_off(connected && controls.off);
(*ui_->stream)->set_privacy_unsupported(controls.unsupported);
(*ui_->stream)->set_privacy_can_toggle(
props->control_mouse_ && connected && controls.can_toggle);
const int language = localization_language_index_;
std::string text = !connected || !props->privacy_status_received_
? localization::privacy_screen[language]
: controls.unsupported ? localization::privacy_unsupported[language]
: controls.enabled ? localization::privacy_on[language]
: controls.off ? localization::privacy_off[language]
: localization::privacy_screen[language];
(*ui_->stream)->set_privacy_status_text(UiText(text));
}
int remote_cursor_shape =
static_cast<int>(RemoteCursorShape::default_cursor);
bool remote_cursor_active = false;
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
remote_cursor_active =
status == ConnectionStatus::Connected && props->control_mouse_ &&
props->remote_cursor_state_received_;
if (remote_cursor_active) {
remote_cursor_shape = static_cast<int>(
props->cursor_presentation_.Resolve(SDL_GetTicks()));
}
}
(*ui_->stream)->set_remote_cursor_active(remote_cursor_active);
(*ui_->stream)->set_remote_cursor_shape(remote_cursor_shape);
(*ui_->stream)->set_audio_enabled(props->audio_capture_button_pressed_);
{
std::lock_guard lock(props->video_settings_mutex_);
if (props->video_settings_pending_ &&
SDL_GetTicks() - props->video_settings_request_tick_ > 5000) {
props->video_settings_pending_ = false;
props->video_settings_failed_ = true;
const auto request_id = props->video_settings_.request_id;
props->video_settings_ = props->applied_video_settings_;
props->video_settings_.request_id = request_id;
}
(*ui_->stream)
->set_video_settings_enabled(status == ConnectionStatus::Connected &&
props->video_settings_supported_);
(*ui_->stream)
->set_video_settings_feedback(UiText(
props->video_settings_failed_ ? localization::video_settings_failed
[localization_language_index_]
: props->video_settings_pending_
? localization::video_settings_pending
[localization_language_index_]
: ""));
(*ui_->stream)->set_video_quality_index(props->video_settings_.quality);
(*ui_->stream)
->set_frame_rate_index(props->video_settings_.frame_rate == 30 ? 0 : 1);
(*ui_->stream)
->set_adaptation_policy_index(props->video_settings_.preference);
}
#if defined(__APPLE__)
fullscreen_button_pressed_ = IsStreamWindowFullscreen();
#else
fullscreen_button_pressed_ = (*ui_->stream)->window().is_fullscreen();
#endif
(*ui_->stream)->set_fullscreen_enabled(fullscreen_button_pressed_);
(*ui_->stream)->set_stats_visible(props->net_traffic_stats_button_pressed_);
std::vector<slint::SharedString> displays;
displays.reserve(props->display_info_list_.size());
for (size_t index = 0; index < props->display_info_list_.size(); ++index) {
displays.emplace_back(UiText(localization::FormatDisplayLabel(
index, props->display_info_list_[index].name,
localization_language_index_)));
}
ui_->display_model->set_vector(std::move(displays));
(*ui_->stream)->set_selected_display(props->selected_display_);
const auto net = props->net_traffic_stats_.Load();
(*ui_->stream)->set_stats_encryption_enabled(
status == ConnectionStatus::Connected && net.srtp_active);
std::vector<ui::NetworkStatsRow> stats_rows;
stats_rows.reserve(4);
const auto append_stats_row = [&](const std::string& label,
const auto& inbound, const auto& outbound) {
ui::NetworkStatsRow row;
row.label = UiText(label);
row.inbound = UiText(FormatBitrate(inbound.bitrate));
row.outbound = UiText(FormatBitrate(outbound.bitrate));
row.loss_rate = UiText(FormatLossRate(inbound.loss_rate));
stats_rows.push_back(std::move(row));
};
append_stats_row(localization::video[localization_language_index_],
net.video_inbound_stats, net.video_outbound_stats);
append_stats_row(localization::audio[localization_language_index_],
net.audio_inbound_stats, net.audio_outbound_stats);
append_stats_row(localization::data[localization_language_index_],
net.data_inbound_stats, net.data_outbound_stats);
append_stats_row(localization::total[localization_language_index_],
net.total_inbound_stats, net.total_outbound_stats);
ui_->stats_model->set_vector(std::move(stats_rows));
(*ui_->stream)
->set_stats_resolution(UiText(std::to_string(props->video_width_) + "x" +
std::to_string(props->video_height_)));
(*ui_->stream)
->set_stats_connection_mode(
UiText(props->traversal_mode_ == TraversalMode::P2P
? localization::connection_mode_direct
[localization_language_index_]
: localization::connection_mode_relay
[localization_language_index_]));
std::vector<RemoteSession::FileTransferInfo> file_list;
{
std::lock_guard lock(props->file_transfer_.file_transfer_list_mutex_);
file_list = props->file_transfer_.file_transfer_list_;
}
const auto transfer_priority = [](RemoteSession::FileTransferStatus status) {
switch (status) {
case RemoteSession::FileTransferStatus::Sending:
return 0;
case RemoteSession::FileTransferStatus::Completed:
return 1;
case RemoteSession::FileTransferStatus::Queued:
return 2;
case RemoteSession::FileTransferStatus::Failed:
return 3;
}
return 3;
};
std::stable_sort(file_list.begin(), file_list.end(),
[&](const auto& left, const auto& right) {
return transfer_priority(left.status) <
transfer_priority(right.status);
});
std::vector<ui::FileTransferEntry> transfers;
transfers.reserve(file_list.size());
for (const auto& info : file_list) {
ui::FileTransferEntry item;
item.name = UiText(info.file_name);
const auto status_index = static_cast<int>(info.status);
if (status_index == 0)
item.status = UiText(localization::queued[localization_language_index_]);
if (status_index == 1)
item.status = UiText(localization::sending[localization_language_index_]);
if (status_index == 2)
item.status =
UiText(localization::completed[localization_language_index_]);
if (status_index == 3)
item.status = UiText(localization::failed[localization_language_index_]);
item.progress = status_index == 2 ? 1.0f
: info.file_size == 0
? 0.0f
: std::clamp(static_cast<float>(info.sent_bytes) /
static_cast<float>(info.file_size),
0.0f, 1.0f);
item.speed = UiText(FormatTransferRate(info.rate_bps));
item.size = UiText(FormatFileSize(info.file_size));
transfers.push_back(std::move(item));
}
ui_->transfer_model->set_vector(std::move(transfers));
(*ui_->stream)
->set_file_transfer_visible(
props->file_transfer_.file_transfer_window_visible_);
const auto fps_now = std::chrono::steady_clock::now();
bool refresh_stats = false;
if (!props->net_traffic_stats_button_pressed_) {
props->fps_ = 0;
props->frame_count_ = 0;
props->last_time_ = {};
} else if (props->last_time_.time_since_epoch().count() == 0) {
props->last_time_ = fps_now;
refresh_stats = true;
} else {
const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
fps_now - props->last_time_)
.count();
if (elapsed >= 1000) {
props->fps_ = static_cast<int>(props->frame_count_ * 1000 / elapsed);
props->frame_count_ = 0;
props->last_time_ = fps_now;
refresh_stats = true;
}
}
(*ui_->stream)->set_stats_fps(UiText(std::to_string(props->fps_)));
if (!props->net_traffic_stats_button_pressed_ ||
status != ConnectionStatus::Connected) {
props->video_latency_snapshot_.reset();
} else if (refresh_stats) {
props->video_latency_snapshot_ = props->video_latency_->Get(fps_now);
}
const auto& latency = props->video_latency_snapshot_;
(*ui_->stream)
->set_stats_video_latency(UiText(
latency ? std::to_string(
static_cast<int>(std::lround(latency->average_ms))) +
" ms"
: "—"));
const auto display_rtt = props->net_traffic_stats_.SmoothedRttMs(fps_now);
const bool rtt_available = props->net_traffic_stats_button_pressed_ &&
status == ConnectionStatus::Connected &&
display_rtt.has_value();
std::string connection_latency = "—";
if (rtt_available) {
connection_latency =
*display_rtt < 1
? "<1 ms"
: std::to_string(static_cast<int>(std::lround(*display_rtt))) + " ms";
}
(*ui_->stream)->set_stats_connection_latency(UiText(connection_latency));
if (refresh_stats && status == ConnectionStatus::Connected) {
LOG_DEBUG("Latency stats: remote={} video_ms={} video_samples={} "
"rtt_raw_ms={} rtt_display_ms={}",
props->remote_id_, latency ? latency->average_ms : -1,
latency ? latency->samples : 0, net.rtt_ms,
display_rtt.value_or(-1));
}
}
void GuiApplication::SyncStreamVideoFrame() {
if (!ui_ || !ui_->stream || !ui_->video_presenter) {
return;
}
auto props = SelectedSession();
if (!props) {
return;
}
SlintVideoPresenter::Frame frame;
frame.remote_id = props->remote_id_;
{
std::lock_guard lock(props->video_frame_mutex_);
frame.nv12 = props->front_frame_;
frame.width = props->video_width_;
frame.height = props->video_height_;
frame.sequence = props->video_frame_sequence_;
frame.timing = props->video_frame_timing_;
}
const auto result = ui_->video_presenter->Present(frame);
if (result.width > 0 && result.height > 0) {
(*ui_->stream)
->set_stats_resolution(UiText(std::to_string(result.width) + "x" +
std::to_string(result.height)));
}
if (result.new_frame) {
++props->frame_count_;
}
}
void GuiApplication::ConfigureStreamVideoRenderer() {
if (!ui_ || !ui_->stream || !ui_->video_presenter) {
return;
}
ui_->video_presenter->PrepareWindow(
*ui_->stream,
[this] {
SlintVideoPresenter::SurfaceState state;
state.selected_stream = focused_remote_id_;
state.fullscreen = fullscreen_button_pressed_;
return state;
},
[this] { video_frame_dirty_.store(true, std::memory_order_release); });
}
void GuiApplication::ScheduleNextVideoFrame() {
if (!ui_) {
return;
}
constexpr auto frame_interval = std::chrono::nanoseconds(1'000'000'000 / 60);
const auto now = std::chrono::steady_clock::now();
if (next_video_frame_time_ == std::chrono::steady_clock::time_point{} ||
now - next_video_frame_time_ > 250ms) {
next_video_frame_time_ = now;
}
if (now >= next_video_frame_time_) {
const bool frame_dirty =
video_frame_dirty_.exchange(false, std::memory_order_acq_rel);
const bool native_render_needed = ui_->stream && ui_->video_presenter &&
ui_->video_presenter->NeedsRedraw();
if (frame_dirty || native_render_needed) {
SyncStreamVideoFrame();
}
do {
next_video_frame_time_ += frame_interval;
} while (next_video_frame_time_ <= now);
}
const auto after_render = std::chrono::steady_clock::now();
const auto remaining = std::max(next_video_frame_time_ - after_render,
std::chrono::steady_clock::duration::zero());
const auto delay_ns =
std::chrono::duration_cast<std::chrono::nanoseconds>(remaining).count();
const auto delay_ms = std::max<int64_t>(1, (delay_ns + 999'999) / 1'000'000);
ui_->video_timer.start(slint::TimerMode::SingleShot,
std::chrono::milliseconds(delay_ms),
[this] { ScheduleNextVideoFrame(); });
}
void GuiApplication::SetServerPanelExpanded(bool expanded) {
if (!ui_->server || !ui_->server_bar) return;
server_window_collapsed_ = !expanded;
(*ui_->server_bar)->set_expanded(expanded);
if (expanded) {
PositionServerWindows();
(*ui_->server)->show();
#if _WIN32
ConfigureWindowsServerWindow((*ui_->server)->window());
#endif
PositionServerWindows();
} else {
(*ui_->server)->set_confirming_disconnect(false);
(*ui_->server)->hide();
}
}
void GuiApplication::PositionServerWindows() {
if (!ui_->server || !ui_->server_bar) return;
auto& bar = *ui_->server_bar;
auto& panel = *ui_->server;
const float scale = std::max(bar->window().scale_factor(), 0.01f);
SDL_Rect bounds{};
if (!SDL_GetDisplayUsableBounds(ui_->server_display, &bounds)) {
ui_->server_display = SDL_GetPrimaryDisplay();
if (!SDL_GetDisplayUsableBounds(ui_->server_display, &bounds)) return;
}
// Cocoa/SDL uses points; Windows and X11 report desktop pixels.
#if defined(__APPLE__)
const float desktop_scale = 1;
#else
const float desktop_scale = scale;
#endif
const server_window_state::Rect screen{
bounds.x / desktop_scale, bounds.y / desktop_scale,
bounds.w / desktop_scale, bounds.h / desktop_scale};
float pointer_x = 0, pointer_y = 0;
bool pressed = false;
const bool pointer_valid = CanUseGlobalPointerPosition() &&
ui_->server_drag.ReadGlobalPointer(&pointer_x, &pointer_y, &pressed);
if (pointer_valid && ui_->server_drag.active) {
const float delta = (pointer_y - ui_->server_drag.pointer_start_y) / desktop_scale;
if (std::abs(delta) >= 4) {
ui_->server_drag_moved = true;
bar->set_dragged(true);
}
if (ui_->server_drag_moved) {
const float travel = std::max(1.0f, screen.height - 52.0f);
ui_->server_anchor = std::clamp(ui_->server_drag_anchor + delta / travel, 0.0f, 1.0f);
}
if (!pressed) ui_->server_drag.active = false;
}
ui_->server_layout = server_window_state::PlacePanel(
screen, ui_->server_anchor, bar->get_bar_width(), panel->get_content_height());
const auto& layout = ui_->server_layout;
bar->set_opens_above(layout.above);
const auto apply = [scale](slint::Window& window, const server_window_state::Rect& rect) {
const auto size = window.size();
const float own_scale = std::max(window.scale_factor(), 0.01f);
if (std::abs(size.width / own_scale - rect.width) > 0.5f ||
std::abs(size.height / own_scale - rect.height) > 0.5f) {
window.set_size(slint::LogicalSize(slint::Size<float>{rect.width, rect.height}));
}
if (!CanUseGlobalPointerPosition()) return;
const auto position = window.position();
if (std::abs(position.x / scale - rect.x) <= 0.5f &&
std::abs(position.y / scale - rect.y) <= 0.5f) return;
#if defined(__APPLE__)
window.set_position(slint::LogicalPosition(slint::Point<float>{rect.x, rect.y}));
#else
window.set_position(slint::PhysicalPosition(slint::Point<int32_t>{
static_cast<int32_t>(std::lround(rect.x * scale)),
static_cast<int32_t>(std::lround(rect.y * scale))}));
#endif
};
apply(bar->window(), layout.bar);
apply(panel->window(), layout.panel);
if (pointer_valid) {
const float x = pointer_x / desktop_scale;
const float y = pointer_y / desktop_scale;
if (pressed && !ui_->server_pointer_pressed && !server_window_collapsed_ &&
!ui_->server_file_dialog_open && !ui_->server_drag.active &&
!server_window_state::Contains(layout.bar, x, y) &&
!server_window_state::Contains(layout.panel, x, y)) {
SetServerPanelExpanded(false);
}
ui_->server_pointer_pressed = pressed;
}
}
void GuiApplication::SyncServerWindow() {
// The connection map is authoritative. Lifecycle flags record callback
// intent, but callbacks for different controllers can cross each other, so
// the final decision must reflect the current connected-controller set.
need_to_create_server_window_.exchange(false, std::memory_order_acq_rel);
need_to_destroy_server_window_.exchange(false, std::memory_order_acq_rel);
bool has_connected_controller = false;
{
std::shared_lock lock(connection_status_mutex_);
has_connected_controller =
std::any_of(connection_status_.begin(), connection_status_.end(),
[](const auto& entry) {
return entry.second == ConnectionStatus::Connected;
});
}
// Privacy protects local display output while preserving capture.
// Keep the panel alive throughout privacy transitions so remote
// controllers can still see and operate it without resetting its UI state.
bool show_controller_window = has_connected_controller;
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__)
// Preserve the existing panel behavior on other platforms.
const auto privacy_status = privacy_.Snapshot();
const bool privacy_covering = privacy_status.overlay_active ||
privacy_status.state == PrivacyState::starting ||
privacy_status.state == PrivacyState::stopping;
show_controller_window = show_controller_window && !privacy_covering;
#endif
if (show_controller_window && !ui_->server) {
ui_->server.emplace(ui::ServerWindow::create());
ui_->server_bar.emplace(ui::ServerStatusBar::create());
RegisterFontAwesome((*ui_->server)->window());
RegisterFontAwesome((*ui_->server_bar)->window());
(*ui_->server)->set_controllers(ui_->controller_model);
server_window_collapsed_ = true;
ui_->server_display = DisplayForSlintWindow(ui_->main->window());
ui_->server_anchor = 0.3f;
ui_->server_drag.active = false;
ui_->server_drag_moved = false;
ui_->server_pointer_pressed = false;
ui_->server_suspended_for_dialog = false;
BindServerCallbacks();
ui_localization::ApplyServerStrings(*ui_->server, localization_language_index_);
ui_localization::ApplyServerStrings(*ui_->server_bar, localization_language_index_);
(*ui_->server_bar)->show();
#if _WIN32
ui_->server_native_window_attempts = 30;
#endif
server_window_created_ = true;
server_window_inited_ = true;
}
if (!show_controller_window && ui_->server) {
(*ui_->server)->hide();
(*ui_->server_bar)->hide();
// Keep handles alive through a native file dialog's nested event loop.
if (ui_->server_file_dialog_open) {
server_window_collapsed_ = true;
(*ui_->server_bar)->set_expanded(false);
(*ui_->server)->set_confirming_disconnect(false);
ui_->server_suspended_for_dialog = true;
return;
}
ui_->server.reset();
ui_->server_bar.reset();
ui_->server_drag.active = false;
server_window_created_ = false;
server_window_inited_ = false;
#if _WIN32
ui_->server_native_window_attempts = 0;
#endif
}
if (!ui_->server) return;
if (ui_->server_suspended_for_dialog) {
(*ui_->server_bar)->show();
ui_->server_suspended_for_dialog = false;
}
std::vector<ui::ControllerEntry> controllers;
{
std::shared_lock lock(connection_status_mutex_);
controllers.reserve(connection_status_.size());
for (const auto& [id, status] : connection_status_) {
if (status != ConnectionStatus::Connected) continue;
const auto host = connection_host_names_.find(id);
const std::string name =
host != connection_host_names_.end() && !host->second.empty()
? host->second : id;
controllers.push_back({UiText(id), UiText(name)});
}
}
// Stable ordering and model identity preserve radio selection, hover and
// scrolling while the 16 ms UI timer reconciles asynchronous peer events.
std::sort(controllers.begin(), controllers.end(), [](const auto& a, const auto& b) {
return std::string(a.remote_id) < std::string(b.remote_id);
});
std::vector<std::string> ids;
std::string signature;
for (const auto& controller : controllers) {
ids.emplace_back(controller.remote_id);
signature += std::string(controller.remote_id) + '\0' +
std::string(controller.display_name) + '\0';
}
if (ids != ui_->controller_ids) (*ui_->server)->set_confirming_disconnect(false);
ui_->controller_ids = std::move(ids);
if (signature != ui_->controller_model_signature) {
ui_->controller_model->set_vector(std::move(controllers));
ui_->controller_model_signature = std::move(signature);
}
const int selected = server_window_state::ReconcileSelectedController(
ui_->controller_ids, &selected_server_remote_id_);
(*ui_->server)->set_selected_controller(selected);
(*ui_->server_bar)->set_controller_count(static_cast<int>(ui_->controller_ids.size()));
ui_localization::ApplyServerStrings(*ui_->server, localization_language_index_);
ui_localization::ApplyServerStrings(*ui_->server_bar, localization_language_index_);
#if _WIN32
if (ui_->server_native_window_attempts > 0) {
if (ConfigureWindowsServerWindow((*ui_->server_bar)->window()))
ui_->server_native_window_attempts = 0;
else
--ui_->server_native_window_attempts;
}
#endif
auto& transfer = transfers_.global_state();
const auto sent = transfer.file_sent_bytes_.load();
const auto total = transfer.file_total_bytes_.load();
(*ui_->server)
->set_file_transfer_visible(transfer.file_transfer_window_visible_);
(*ui_->server)->set_sending_file(transfer.file_sending_.load());
(*ui_->server)
->set_file_progress(total == 0 ? 0.0f
: std::clamp(static_cast<float>(sent) /
static_cast<float>(total),
0.0f, 1.0f));
(*ui_->server)
->set_file_progress_text(
UiText(FormatTransferRate(transfer.file_send_rate_bps_.load())));
std::string current_file_name;
const uint32_t current_file_id = transfer.current_file_id_.load();
if (current_file_id != 0) {
std::lock_guard lock(transfer.file_transfer_list_mutex_);
for (const auto& info : transfer.file_transfer_list_) {
if (info.file_id == current_file_id) {
current_file_name = info.file_name;
break;
}
}
}
(*ui_->server)->set_current_file_name(UiText(current_file_name));
(*ui_->server)
->set_file_size_text(total == 0 ? slint::SharedString{}
: UiText(FormatFileSize(sent) + " / " +
FormatFileSize(total)));
PositionServerWindows();
}
bool GuiApplication::ValidateSelfHostedSettingsFromUi() {
auto& main = ui_->main;
const auto address = minirtc::ParseSignalServerAddress(
std::string(main->get_server_host()));
if (!address) {
main->set_self_host_settings_error(UiText(
localization::self_hosted_address_invalid[localization_language_index_]));
return false;
}
const auto port = address->port ? address->port : ParsePort(main->get_server_port());
if (!port) {
main->set_self_host_settings_error(UiText(
localization::self_hosted_port_invalid[localization_language_index_]));
return false;
}
main->set_server_host(UiText(address->Address()));
main->set_server_port(UiText(std::to_string(*port)));
main->set_self_host_settings_error("");
return true;
}
bool GuiApplication::SaveSettingsFromUi() {
const auto previous_server = GetSignalServerSettings(*config_center_);
auto& main = ui_->main;
const bool has_endpoint = main->get_self_hosted_enabled() ||
!std::string(main->get_server_host()).empty() ||
!std::string(main->get_server_port()).empty();
if (has_endpoint && !ValidateSelfHostedSettingsFromUi()) {
main->set_self_host_settings_open(true);
return false;
}
if ((has_endpoint && config_center_->SetServerEndpoint(
std::string(main->get_server_host()), *ParsePort(main->get_server_port())) != 0) ||
config_center_->SetSelfHosted(main->get_self_hosted_enabled()) != 0) {
main->set_self_host_settings_error(UiText(
localization::self_hosted_save_failed[localization_language_index_]));
main->set_self_host_settings_open(true);
return false;
}
language_button_value_ =
localization::detail::ClampLanguageIndex(main->get_language_index());
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
enable_hardware_video_codec_ = ConfigCenter::IsHardwareVideoCodecAvailable() &&
main->get_hardware_codec_enabled();
enable_turn_ = main->get_turn_enabled();
enable_self_hosted_ = main->get_self_hosted_enabled();
enable_autostart_ = main->get_autostart_enabled();
enable_daemon_ = main->get_daemon_enabled();
localization_language_ =
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
localization_language_index_ = language_button_value_;
config_center_->SetLanguage(localization_language_);
privacy_.SetText(
{localization::privacy_screen_unlock_hint[localization_language_index_]});
config_center_->SetVideoEncodeFormat(
static_cast<ConfigCenter::VIDEO_ENCODE_FORMAT>(
video_encode_format_button_value_));
config_center_->SetHardwareVideoCodec(enable_hardware_video_codec_);
config_center_->SetTurn(enable_turn_, main->get_force_relay_enabled());
main->set_force_relay_enabled(config_center_->IsForceRelay());
config_center_->SetAutostart(enable_autostart_);
config_center_->SetDaemon(enable_daemon_);
if (config_center_->SetSaveRemotePreviews(main->get_save_remote_previews()) != 0) {
LOG_ERROR("Failed to save remote preview preference");
}
if (thumbnail_->SetSavePreviews(config_center_->IsSaveRemotePreviews()) != 0) {
LOG_ERROR("Failed to clear saved remote previews");
}
reload_recent_connections_ = true;
#ifdef _WIN32
if (!HasActiveSession()) {
const auto previous_capture_method = config_center_->GetScreenCaptureMethod();
const auto capture_method = static_cast<ScreenCaptureMethod>(
std::clamp(main->get_capture_method_index(), 0, 3));
if (config_center_->SetScreenCaptureMethod(capture_method) != 0) {
LOG_ERROR("Failed to save screen capture method");
} else if (capture_method != previous_capture_method) {
// Reconfigure only capture; its backend does not own signaling.
devices_.StopScreenCapturer();
screen_capturer_is_started_ = false;
if (devices_.InitializeScreenCapturer() != 0) {
LOG_ERROR("Failed to apply screen capture method");
}
}
}
main->set_capture_method_index(
static_cast<int>(config_center_->GetScreenCaptureMethod()));
#endif
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
if (config_center_->SetPrivacyScreen(main->get_privacy_on_connect_enabled()) !=
0) {
main->set_privacy_on_connect_enabled(
config_center_->IsEnablePrivacyScreen());
}
#endif
const std::string path(main->get_file_save_path());
std::memset(file_transfer_save_path_buf_, 0,
sizeof(file_transfer_save_path_buf_));
std::strncpy(file_transfer_save_path_buf_, path.c_str(),
sizeof(file_transfer_save_path_buf_) - 1);
config_center_->SetFileTransferSavePath(path);
ApplySettingsFromConfig(previous_server !=
GetSignalServerSettings(*config_center_));
return true;
}
void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
language_button_value_ = static_cast<int>(config_center_->GetLanguage());
localization_language_index_ = localization::detail::ClampLanguageIndex(language_button_value_);
localization_language_ = config_center_->GetLanguage();
video_encode_format_button_value_ = static_cast<int>(config_center_->GetVideoEncodeFormat());
enable_hardware_video_codec_ = config_center_->IsHardwareVideoCodec();
enable_turn_ = config_center_->IsEnableTurn();
enable_self_hosted_ = config_center_->IsSelfHosted();
enable_autostart_ = config_center_->IsEnableAutostart();
enable_daemon_ = config_center_->IsEnableDaemon();
privacy_.SetText({localization::privacy_screen_unlock_hint[localization_language_index_]});
const auto path = config_center_->GetFileTransferSavePath();
std::snprintf(file_transfer_save_path_buf_, sizeof(file_transfer_save_path_buf_), "%s", path.c_str());
const auto host = config_center_->GetSignalServerHost();
std::snprintf(signal_server_ip_self_, sizeof(signal_server_ip_self_), "%s", host.c_str());
const int port = config_center_->GetSignalServerPort();
const auto port_text = port > 0 ? std::to_string(port) : std::string{};
std::snprintf(signal_server_port_self_, sizeof(signal_server_port_self_), "%s", port_text.c_str());
language_button_value_last_ = language_button_value_;
video_encode_format_button_value_last_ = video_encode_format_button_value_;
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
enable_turn_last_ = enable_turn_;
enable_self_hosted_last_ = enable_self_hosted_;
enable_autostart_last_ = enable_autostart_;
enable_daemon_last_ = enable_daemon_;
file_transfer_save_path_last_ = path;
#ifdef __linux__
HeadlessConsole::Instance().SetLanguage(localization_language_index_);
#endif
if (ui_) {
ui_->localized_language = -1;
ResetSettingsUi();
}
if (reconnect && !HasActiveSession()) {
// Tick recreates the peer after asynchronous cleanup, using the committed
// configuration. Keep the application and its console running throughout.
CloseAllRemoteSessions();
} else {
const bool hardware = config_center_->IsHardwareVideoCodec();
const bool av1 = config_center_->GetVideoEncodeFormat() ==
ConfigCenter::VIDEO_ENCODE_FORMAT::AV1;
const auto turn = static_cast<TurnMode>(config_center_->GetTurnMode());
if (hardware != params_.hardware_acceleration || av1 != params_.av1_encoding ||
turn != params_.turn_mode) {
// Update the listening peer for incoming connections and the template
// copied by ConnectTo for outgoing connections. Existing sessions retain
// the codec/ICE snapshot with which they negotiated.
if (!peer_ || UpdateConnectionSettings(peer_, hardware, av1, turn) == 0) {
params_.hardware_acceleration = hardware;
params_.av1_encoding = av1;
params_.turn_mode = turn;
} else {
LOG_ERROR("Failed to apply connection settings without reconnecting");
}
}
}
}
void GuiApplication::ConnectFromUi(const std::string& input) {
const std::string remote_id = CompactPeerId(input);
if (remote_id.empty()) {
return;
}
if (auto existing = FindRemoteSession(remote_id);
existing && existing->connection_established_) {
focused_remote_id_ = remote_id;
need_to_create_stream_window_ = true;
return;
}
for (const auto& [_, connection] : recent_connections_) {
if (connection.remote_id == remote_id) {
ConnectTo(remote_id, connection.password.c_str(),
connection.remember_password);
return;
}
}
ConnectTo(remote_id, "", false);
}
void GuiApplication::SelectStreamTab(int index) {
if (!ui_ || index < 0 || index >= static_cast<int>(ui_->tab_ids.size())) {
return;
}
const std::string selected_remote_id = ui_->tab_ids[index];
const bool selection_changed = focused_remote_id_ != selected_remote_id;
bool renderer_selection_changed = false;
if (!controlled_remote_id_.empty() &&
controlled_remote_id_ != selected_remote_id) {
devices_.StopKeyboardCapturer();
keyboard_.ForceReleasePressedKeys();
}
focused_remote_id_ = selected_remote_id;
controlled_remote_id_ = selected_remote_id;
if (ui_->video_presenter) {
renderer_selection_changed =
ui_->video_presenter->SelectStream(selected_remote_id);
}
if (renderer_selection_changed) {
video_frame_dirty_.store(true, std::memory_order_release);
}
if (selection_changed && ui_->stream) {
(*ui_->stream)->set_has_frame(false);
}
std::shared_ptr<RemoteSession> selected_session;
{
std::shared_lock lock(remote_sessions_mutex_);
for (auto& [id, props] : remote_sessions_) {
if (props) {
props->tab_selected_ = id == focused_remote_id_;
}
}
const auto selected = remote_sessions_.find(selected_remote_id);
if (selected != remote_sessions_.end()) {
selected_session = selected->second;
}
start_keyboard_capturer_ = selected_session &&
selected_session->control_mouse_ &&
selected_session->connection_status_.load() ==
ConnectionStatus::Connected;
}
}
void GuiApplication::ReorderStreamTab(int from, float drop_x, float tab_width) {
if (!ui_ || from < 0 || from >= static_cast<int>(ui_->tab_order.size()) ||
tab_width <= 0.0f) {
return;
}
const float slot_width = tab_width + 1.0f;
const int target =
std::clamp(static_cast<int>(std::floor(drop_x / slot_width)), 0,
static_cast<int>(ui_->tab_order.size()) - 1);
if (target == from) {
return;
}
std::string moved = std::move(ui_->tab_order[from]);
ui_->tab_order.erase(ui_->tab_order.begin() + from);
ui_->tab_order.insert(ui_->tab_order.begin() + target, std::move(moved));
ui_->tab_ids = ui_->tab_order;
SelectStreamTab(target);
}
void GuiApplication::CloseRemoteClosedTabs() {
std::vector<std::string> closed;
{
std::shared_lock lock(remote_sessions_mutex_);
for (const auto& [id, props] : remote_sessions_) {
if (props && props->remote_close_pending_.exchange(false)) {
closed.push_back(id);
}
}
}
// CloseRemoteSession drains transport callbacks and takes the session map
// lock. Do this on the UI thread, outside both the callback and map lock.
for (const auto& id : closed) CloseStreamTab(id);
}
void GuiApplication::CloseStreamTab(const std::string& remote_id) {
std::shared_ptr<RemoteSession> props;
{
std::shared_lock lock(remote_sessions_mutex_);
if (const auto found = remote_sessions_.find(remote_id);
found != remote_sessions_.end()) {
props = found->second;
}
}
if (!props) {
return;
}
keyboard_.ReleaseRemotePressedKeys(remote_id, "stream_tab_closed");
CloseRemoteSession(props);
ResetRemoteSessionResources(props);
{
std::unique_lock lock(remote_sessions_mutex_);
remote_sessions_.erase(remote_id);
}
if (ui_->video_presenter) {
ui_->video_presenter->ForgetStream(remote_id);
}
if (focused_remote_id_ == remote_id) {
focused_remote_id_.clear();
controlled_remote_id_.clear();
if (ui_->video_presenter && ui_->video_presenter->SelectStream({})) {
video_frame_dirty_.store(true, std::memory_order_release);
}
}
}
std::shared_ptr<GuiApplication::RemoteSession>
GuiApplication::SelectedSession() {
std::shared_lock lock(remote_sessions_mutex_);
auto found = remote_sessions_.find(focused_remote_id_);
if (found != remote_sessions_.end()) {
return found->second;
}
for (const auto& [_, props] : remote_sessions_) {
if (props && props->tab_selected_) {
return props;
}
}
return nullptr;
}
void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
auto props = SelectedSession();
if (!props || !props->peer_ || !props->control_mouse_ || !ui_->stream) {
return;
}
std::optional<VideoPoint> point;
{
std::lock_guard lock(props->video_frame_mutex_);
point = MapVideoPoint(x, y, (*ui_->stream)->get_video_area_width(),
(*ui_->stream)->get_video_area_height(),
props->video_width_, props->video_height_);
}
if (!point) {
return;
}
RemoteAction action{};
action.type = ControlType::mouse;
action.m.x = point->x;
action.m.y = point->y;
// Slint enum order: Cancel=0, Down=1, Up=2, Move=3 and
// Other=0, Left=1, Right=2, Middle=3.
if (kind == 3) {
action.m.flag = MouseFlag::move;
} else if (kind == 1) {
if (button == 1)
action.m.flag = MouseFlag::left_down;
else if (button == 2)
action.m.flag = MouseFlag::right_down;
else if (button == 3)
action.m.flag = MouseFlag::middle_down;
else
return;
} else if (kind == 2) {
if (button == 1)
action.m.flag = MouseFlag::left_up;
else if (button == 2)
action.m.flag = MouseFlag::right_up;
else if (button == 3)
action.m.flag = MouseFlag::middle_up;
else
return;
} else {
return;
}
controlled_remote_id_ = props->remote_id_;
const std::string message = action.to_json();
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->cursor_presentation_.NoteInput(SDL_GetTicks());
}
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
}
void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
float y) {
auto props = SelectedSession();
if (!props || !props->peer_ || !props->control_mouse_ || !ui_->stream) {
return;
}
std::optional<VideoPoint> point;
{
std::lock_guard lock(props->video_frame_mutex_);
point = MapVideoPoint(x, y, (*ui_->stream)->get_video_area_width(),
(*ui_->stream)->get_video_area_height(),
props->video_width_, props->video_height_);
}
if (!point) {
return;
}
RemoteAction action{};
action.type = ControlType::mouse;
action.m.x = point->x;
action.m.y = point->y;
if (std::abs(delta_y) >= std::abs(delta_x)) {
action.m.flag = MouseFlag::wheel_vertical;
action.m.s = delta_y > 0 ? 1 : delta_y < 0 ? -1 : 0;
} else {
action.m.flag = MouseFlag::wheel_horizontal;
action.m.s = delta_x > 0 ? 1 : delta_x < 0 ? -1 : 0;
}
if (action.m.s == 0) {
return;
}
{
std::lock_guard lock(props->remote_cursor_state_mutex_);
props->cursor_presentation_.NoteInput(SDL_GetTicks());
}
controlled_remote_id_ = props->remote_id_;
const std::string message = action.to_json();
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
props->mouse_label_.c_str());
}
void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
bool control, bool alt, bool shift,
bool meta) {
(void)control;
(void)alt;
(void)shift;
(void)meta;
auto props = SelectedSession();
if (!props || !props->peer_ || !props->control_mouse_ ||
props->connection_status_.load() != ConnectionStatus::Connected) {
return;
}
controlled_remote_id_ = props->remote_id_;
focused_remote_id_ = props->remote_id_;
// Native capture forwards the event independently. Slint keyboard events
// are used only by platforms whose native capture backend is unavailable.
if (keyboard_capturer_is_started_ && !keyboard_capturer_uses_window_events_) {
return;
}
const int virtual_key = SlintKeyToWindowsVk(text);
if (virtual_key >= 0) {
keyboard_.SendKeyCommand(virtual_key, pressed);
}
}
std::string GuiApplication::OpenFileDialog(const std::string& title) {
#if defined(__APPLE__)
return OpenNativeFileDialog(title);
#else
const char* path =
tinyfd_openFileDialog(title.c_str(), "", 0, nullptr, nullptr, 0);
return path ? path : "";
#endif
}
bool GuiApplication::OpenUrl(const std::string& url) {
return SDL_OpenURL(url.c_str());
}
void GuiApplication::Cleanup() {
#ifdef __linux__
HeadlessConsole::Instance().BindSettings(nullptr, {}, {});
#endif
if (!ui_) {
SDL_Quit();
return;
}
ui_->timer.stop();
ui_->server_file_dialog_timer.stop();
ui_->video_timer.stop();
#if _WIN32 && CROSSDESK_PORTABLE
JoinPortableWindowsServiceInstallThread();
#endif
privacy_.Disable();
clipboard_.Shutdown();
keyboard_.ForceReleasePressedKeys();
keyboard_.ReleaseAllRemotePressedKeys("application_exit");
devices_.DestroyDevices();
privacy_.Shutdown();
devices_.DestroyFactories();
CloseAllRemoteSessions();
WaitForSessionCleanup();
devices_.DestroyAudioOutput();
if (ui_->stream) {
#if defined(__APPLE__)
void* stream_view = (*ui_->stream)->window().appkit_view();
SetStreamWindowFullscreen(false);
UnregisterStreamWindow(stream_view);
#endif
if (ui_->video_presenter) {
ui_->video_presenter->Detach();
}
(*ui_->stream)->hide();
ui_->stream.reset();
}
if (ui_->server) {
(*ui_->server)->hide();
(*ui_->server_bar)->hide();
}
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
if (ui_->tray) {
ui_->tray->RemoveTrayIcon();
ui_->tray.reset();
}
#endif
ui_.reset();
if (update_checker_) {
update_checker_->Stop();
}
SDL_Quit();
}
} // namespace crossdesk