fix: refine Wayland window behavior and titlebars

This commit is contained in:
dijunkun
2026-07-23 20:35:40 +08:00
parent 3a72e4849e
commit 6f5faa7111
6 changed files with 696 additions and 22 deletions
+290 -9
View File
@@ -28,12 +28,17 @@
#include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include "localization.h"
#include "platform.h"
#if _WIN32
#include "platform/tray/win_tray.h"
#elif defined(__APPLE__)
#include "platform/tray/mac_tray.h"
#include "platform/window_drag.h"
#elif defined(__linux__)
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <unistd.h>
#include "platform/tray/linux_tray.h"
#endif
#include "rd_log.h"
@@ -44,6 +49,140 @@ namespace {
using namespace std::chrono_literals;
#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);
@@ -496,6 +635,12 @@ float ServerWindowLogicalHeight(bool collapsed) {
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 float scale = std::max(window.scale_factor(), 0.01f);
@@ -682,6 +827,7 @@ struct GuiApplication::SlintUi {
std::string recent_model_signature;
slint::Timer timer;
WindowDragState main_drag;
WindowDragState stream_drag;
WindowDragState server_drag;
int main_native_titlebar_attempts = 30;
int stream_live_resize_configuration_attempts = 0;
@@ -697,7 +843,17 @@ struct GuiApplication::SlintUi {
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;
@@ -792,6 +948,24 @@ bool GuiApplication::InitializeSDL() {
if (!getenv("SDL_AUDIODRIVER")) {
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
}
// 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());
@@ -816,9 +990,25 @@ void GuiApplication::InitializeModules() {
}
void GuiApplication::InitializeUi() {
#if defined(__linux__) && !defined(__APPLE__)
// 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");
}
#endif
ui_ = std::make_unique<SlintUi>();
#if _WIN32
ui_->main->set_custom_titlebar(true);
#elif defined(__linux__)
if (use_xwayland_gui_) {
ui_->main->set_custom_titlebar(true);
ui_->main->set_wayland_titlebar(true);
}
#endif
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
for (const auto& parsed :
@@ -924,6 +1114,9 @@ void GuiApplication::InitializeUi() {
if (ui_->capture_mode && ui_->capture_page == "stream") {
ui_->stream.emplace(ui::StreamWindow::create());
#if defined(__linux__)
(*ui_->stream)->set_custom_titlebar(use_xwayland_gui_);
#endif
RegisterFontAwesome((*ui_->stream)->window());
(*ui_->stream)->set_tabs(ui_->tab_model);
(*ui_->stream)->set_displays(ui_->display_model);
@@ -968,7 +1161,8 @@ void GuiApplication::InitializeUi() {
(*ui_->stream)->show();
PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
kStreamWindowLogicalWidth,
kStreamWindowLogicalHeight);
StreamWindowLogicalHeight(
(*ui_->stream)->get_custom_titlebar()));
ui_->main->hide();
} else if (ui_->capture_mode && ui_->capture_page == "server") {
ui_->server.emplace(ui::ServerWindow::create());
@@ -1362,7 +1556,7 @@ void GuiApplication::BindStreamCallbacks() {
return;
}
auto& stream = *ui_->stream;
stream->window().on_close_requested([this] {
auto close_stream_sessions = [this] {
std::vector<std::string> ids;
{
std::shared_lock lock(remote_sessions_mutex_);
@@ -1373,8 +1567,35 @@ void GuiApplication::BindStreamCallbacks() {
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);
@@ -1622,6 +1843,9 @@ void GuiApplication::Tick() {
SyncPlatformDialogs();
SyncStreamWindow();
SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
SyncXWaylandWindowActivation();
#endif
}
void GuiApplication::UpdateLocalization() {
@@ -1985,6 +2209,47 @@ void GuiApplication::SyncPlatformDialogs() {
#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) {
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);
}
return;
}
ui_->main->set_window_active(
X11WindowMatches(display, active_window, ui_->main->window()));
if (ui_->stream) {
(*ui_->stream)
->set_window_active(X11WindowMatches(
display, active_window, (*ui_->stream)->window()));
}
}
#endif
void GuiApplication::SyncStreamWindow() {
bool has_sessions = false;
{
@@ -1993,6 +2258,9 @@ void GuiApplication::SyncStreamWindow() {
}
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());
// Slint globals belong to a component tree. Force the next localization
// pass to initialize the newly-created, independent stream window tree.
@@ -2016,6 +2284,8 @@ void GuiApplication::SyncStreamWindow() {
if (!ui_->stream) {
return;
}
(*ui_->stream)
->set_window_maximized((*ui_->stream)->window().is_maximized());
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
#if defined(__APPLE__)
if (ui_->stream_live_resize_configuration_attempts > 0) {
@@ -2029,7 +2299,8 @@ void GuiApplication::SyncStreamWindow() {
if (ui_->stream_initial_position_attempts > 0) {
if (PositionWindowAtCenter((*ui_->stream)->window(), ui_->main->window(),
kStreamWindowLogicalWidth,
kStreamWindowLogicalHeight)) {
StreamWindowLogicalHeight(
(*ui_->stream)->get_custom_titlebar()))) {
--ui_->stream_initial_position_attempts;
} else {
ui_->stream_initial_position_attempts = 0;
@@ -2586,10 +2857,15 @@ void GuiApplication::SendPointerInput(int button, int kind, float x, float y) {
const auto size = (*ui_->stream)->window().size();
const float scale = (*ui_->stream)->window().scale_factor();
const float available_width = size.width / scale;
const float titlebar_height =
!fullscreen_button_pressed_ && (*ui_->stream)->get_custom_titlebar()
? kWaylandTitlebarLogicalHeight
: 0.0f;
const float available_height =
size.height / scale - (fullscreen_button_pressed_
? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
size.height / scale - titlebar_height -
(fullscreen_button_pressed_
? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
float render_width = available_width;
float render_height = available_height;
float offset_x = 0;
@@ -2657,10 +2933,15 @@ void GuiApplication::SendScrollInput(float delta_x, float delta_y, float x,
const auto size = (*ui_->stream)->window().size();
const float scale = (*ui_->stream)->window().scale_factor();
const float width = size.width / scale;
const float titlebar_height =
!fullscreen_button_pressed_ && (*ui_->stream)->get_custom_titlebar()
? kWaylandTitlebarLogicalHeight
: 0.0f;
const float height =
size.height / scale - (fullscreen_button_pressed_
? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
size.height / scale - titlebar_height -
(fullscreen_button_pressed_
? 0.0f
: (ui_->tab_ids.size() > 1 ? 30.0f : 0.0f));
if (width <= 0 || height <= 0) {
return;
}
+6
View File
@@ -36,6 +36,9 @@ private:
void SyncPlatformDialogs();
void SyncStreamWindow();
void SyncServerWindow();
#if defined(__linux__) && !defined(__APPLE__)
void SyncXWaylandWindowActivation();
#endif
void UpdateLocalization();
void ResetSettingsUi();
void SaveSettingsFromUi();
@@ -59,6 +62,9 @@ private:
bool OpenUrl(const std::string &url);
std::unique_ptr<SlintUi> ui_;
#if defined(__linux__) && !defined(__APPLE__)
bool use_xwayland_gui_ = false;
#endif
};
} // namespace crossdesk
+117 -8
View File
@@ -184,16 +184,21 @@ component ConnectionDialogButton inherits Rectangle {
export component MainWindow inherits Window {
title: "CrossDesk";
preferred-width: 640px;
preferred-height: root.custom-titlebar ? 481px : 450px;
preferred-height: 450px + root.titlebar-height;
min-width: 640px;
max-width: 640px;
min-height: root.custom-titlebar ? 481px : 450px;
max-height: root.custom-titlebar ? 481px : 450px;
min-height: 450px + root.titlebar-height;
max-height: 450px + root.titlebar-height;
no-frame: root.custom-titlebar;
background: white;
background: root.wayland-titlebar ? transparent : white;
default-font-size: ImGuiFontStyle.base;
in property <bool> custom-titlebar: false;
in property <bool> wayland-titlebar: false;
in property <bool> window-active: true;
private property <length> titlebar-height:
root.custom-titlebar ? (root.wayland-titlebar ? 32px : 31px) : 0px;
private property <length> wayland-corner-radius: 12px;
in property <string> local-id: "";
in property <string> local-password: "";
in-out property <string> remote-id-input: "";
@@ -310,7 +315,7 @@ export component MainWindow inherits Window {
? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px
: root.recent-tooltip-pointer-x + 12px));
private property <length> recent-tooltip-y: min(
root.height - (root.custom-titlebar ? 31px : 0px) - root.recent-tooltip-height - 8px,
root.height - root.titlebar-height - root.recent-tooltip-height - 8px,
max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px));
reset-remote-id => {
@@ -318,7 +323,108 @@ export component MainWindow inherits Window {
remote-id-editor.focus();
}
if root.custom-titlebar: Rectangle {
window-surface := Rectangle {
width: root.width;
height: root.height;
background: root.wayland-titlebar ? #f6f5f4 : transparent;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
clip: root.wayland-titlebar;
if root.custom-titlebar && root.wayland-titlebar: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 80px;
width: parent.width - 160px;
height: parent.height;
text: "CrossDesk";
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
wayland-title-drag := TouchArea {
width: parent.width - 80px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
root.main-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.main-title-drag(2, self.mouse-x, self.mouse-y);
}
}
}
wayland-minimize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-minimize-touch.pressed ? #c0bfbc
: wayland-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { wayland-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| wayland-minimize-touch.has-hover
|| wayland-minimize-touch.pressed ? #454545 : #898989;
}
wayland-minimize-touch := TouchArea {
clicked => { root.minimize-main-window(); }
}
}
wayland-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: wayland-close-touch.pressed ? #b2161d
: wayland-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { wayland-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: wayland-close-touch.has-hover
|| wayland-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
wayland-close-touch := TouchArea {
clicked => { root.close-main-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
if root.custom-titlebar && !root.wayland-titlebar: Rectangle {
x: 0px;
y: 0px;
width: root.width;
@@ -404,7 +510,7 @@ export component MainWindow inherits Window {
content-layer := Rectangle {
x: 0px;
y: root.custom-titlebar ? 31px : 0px;
y: root.titlebar-height;
width: root.width;
height: root.height - self.y;
clip: true;
@@ -1613,7 +1719,10 @@ export component MainWindow inherits Window {
height: root.height;
background: transparent;
border-width: 1px;
border-color: #8b8b8b;
border-color: root.wayland-titlebar ? #aaa8a6 : #8b8b8b;
border-radius:
root.wayland-titlebar ? root.wayland-corner-radius : 0px;
z: 200;
}
}
}
+184 -5
View File
@@ -112,13 +112,16 @@ component ControlBarButton inherits Rectangle {
export component StreamWindow inherits Window {
title: "CrossDesk";
preferred-width: 1280px;
preferred-height: 720px;
preferred-height: 720px + (root.custom-titlebar ? 32px : 0px);
min-width: 640px;
min-height: 360px;
no-frame: false;
background: black;
min-height: 360px + (root.custom-titlebar ? 32px : 0px);
no-frame: root.custom-titlebar;
background: root.custom-titlebar ? transparent : black;
default-font-size: ImGuiFontStyle.base;
in property <bool> custom-titlebar: false;
in property <bool> window-active: true;
in property <bool> window-maximized: false;
in property <[StreamTab]> tabs;
in-out property <int> selected-tab: 0;
in property <image> frame;
@@ -153,6 +156,10 @@ export component StreamWindow inherits Window {
callback toggle-stats;
callback toggle-fullscreen;
callback disconnect;
callback stream-title-drag(int, length, length);
callback minimize-stream-window;
callback toggle-maximize-stream-window;
callback close-stream-window;
callback pointer-input(PointerEventButton, PointerEventKind, length, length);
callback scroll-input(length, length, length, length);
callback key-input(string, bool, bool, bool, bool, bool);
@@ -169,6 +176,9 @@ export component StreamWindow inherits Window {
private property <length> control-press-x: 0px;
private property <length> control-press-y: 0px;
private property <int> dragging-tab: -1;
private property <length> titlebar-height:
root.custom-titlebar && !root.fullscreen-enabled ? 32px : 0px;
private property <length> window-corner-radius: 12px;
callback begin-control-drag(length, length);
callback move-control-drag(length, length);
callback end-control-drag;
@@ -202,10 +212,167 @@ export component StreamWindow inherits Window {
}
}
Rectangle {
window-surface := Rectangle {
width: root.width;
height: root.height;
background: black;
border-radius:
root.custom-titlebar && !root.fullscreen-enabled && !root.window-maximized
? root.window-corner-radius : 0px;
clip: root.custom-titlebar && !root.fullscreen-enabled;
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
x: 0px;
y: 0px;
width: parent.width;
height: root.titlebar-height;
background: #f6f5f4;
z: 100;
Text {
x: 120px;
width: parent.width - 240px;
height: parent.height;
text: "CrossDesk";
color: root.window-active ? #3d3d3d : #747474;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
overflow: elide;
}
stream-title-drag-area := TouchArea {
width: parent.width - 120px;
property <int> phase: self.pressed ? 1 : 0;
changed phase => {
root.stream-title-drag(phase, self.mouse-x, self.mouse-y);
}
moved => {
if (self.pressed) {
root.stream-title-drag(2, self.mouse-x, self.mouse-y);
}
}
double-clicked => {
root.toggle-maximize-stream-window();
}
}
stream-minimize-button := Rectangle {
x: parent.width - 114px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-minimize-touch.pressed ? #c0bfbc
: stream-minimize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Minimize";
accessible-action-default => { stream-minimize-touch.clicked(); }
Rectangle {
x: 8px;
y: 13px;
width: 8px;
height: 0.75px;
background: root.window-active
|| stream-minimize-touch.has-hover
|| stream-minimize-touch.pressed ? #454545 : #898989;
}
stream-minimize-touch := TouchArea {
clicked => { root.minimize-stream-window(); }
}
}
stream-maximize-button := Rectangle {
x: parent.width - 74px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-maximize-touch.pressed ? #c0bfbc
: stream-maximize-touch.has-hover ? #deddda
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: root.window-maximized ? "Restore" : "Maximize";
accessible-action-default => { stream-maximize-touch.clicked(); }
if !root.window-maximized: Rectangle {
x: 8px;
y: 8px;
width: 8px;
height: 8px;
background: transparent;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
if root.window-maximized: Rectangle {
x: 9px;
y: 7px;
width: 8px;
height: 8px;
background: transparent;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
if root.window-maximized: Rectangle {
x: 7px;
y: 9px;
width: 8px;
height: 8px;
background: stream-maximize-button.background;
border-width: 1px;
border-color: root.window-active
|| stream-maximize-touch.has-hover
|| stream-maximize-touch.pressed ? #454545 : #898989;
}
stream-maximize-touch := TouchArea {
clicked => { root.toggle-maximize-stream-window(); }
}
}
stream-close-button := Rectangle {
x: parent.width - 34px;
y: 4px;
width: 24px;
height: 24px;
border-radius: 12px;
background: stream-close-touch.pressed ? #b2161d
: stream-close-touch.has-hover ? #e01b24
: root.window-active ? #e6e6e6
: transparent;
accessible-role: button;
accessible-label: "Close";
accessible-action-default => { stream-close-touch.clicked(); }
Path {
width: parent.width;
height: parent.height;
viewbox-width: 24;
viewbox-height: 24;
commands: "M 14.75 14.75 L 8.25 8.25 M 14.75 8.25 L 8.25 14.75";
stroke: stream-close-touch.has-hover
|| stream-close-touch.pressed ? white
: root.window-active ? #454545 : #898989;
stroke-width: 1px;
}
stream-close-touch := TouchArea {
clicked => { root.close-stream-window(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: #d5d3d0;
}
}
content-layer := Rectangle {
y: root.titlebar-height;
width: parent.width;
height: parent.height - self.y;
background: black;
clip: true;
// Keep the ImGui tab strip behavior, including pointer-drag reordering.
Rectangle {
@@ -645,5 +812,17 @@ export component StreamWindow inherits Window {
}
}
}
}
if root.custom-titlebar && !root.fullscreen-enabled: Rectangle {
width: parent.width;
height: parent.height;
background: transparent;
border-width: 1px;
border-color: #aaa8a6;
border-radius:
root.window-maximized ? 0px : root.window-corner-radius;
z: 200;
}
}
}
+98
View File
@@ -1,19 +1,58 @@
#include "crossdesk_ui.h"
#include "fa_solid_900.h"
#include <cassert>
#include <cstdlib>
#include <fstream>
#include <memory>
#include <string>
#include <vector>
namespace {
// Set the CROSSDESK_*_UI_SNAPSHOT variables while using Slint's software
// renderer to export deterministic active and inactive visual-check images.
bool WriteWindowSnapshot(const slint::Window &window, const char *path) {
const auto snapshot = window.take_snapshot();
if (!snapshot) {
return false;
}
std::ofstream output(path, std::ios::binary);
output << "P6\n"
<< snapshot->width() << ' ' << snapshot->height() << "\n255\n";
for (const auto &pixel : *snapshot) {
output.put(static_cast<char>(pixel.r));
output.put(static_cast<char>(pixel.g));
output.put(static_cast<char>(pixel.b));
}
return output.good();
}
bool RegisterFontAwesome(slint::Window &window) {
return !window.window_handle()
.register_font_from_data(fa_solid_900_ttf,
fa_solid_900_ttf_len)
.has_value();
}
} // namespace
int main() {
auto window = crossdesk::ui::MainWindow::create();
auto stream = crossdesk::ui::StreamWindow::create();
auto server = crossdesk::ui::ServerWindow::create();
if (!RegisterFontAwesome(window->window()) ||
!RegisterFontAwesome(stream->window()) ||
!RegisterFontAwesome(server->window())) {
return 1;
}
window->set_local_id("123 456 789");
window->set_local_password("123456");
window->set_connection_dialog_open(true);
window->set_connection_password_required(true);
window->set_custom_titlebar(true);
window->set_wayland_titlebar(true);
window->set_settings_session_active(true);
window->set_hardware_codec_available(false);
window->set_portable_service_settings_visible(true);
@@ -21,9 +60,32 @@ int main() {
assert(window->get_connection_dialog_open());
assert(window->get_connection_password_required());
assert(window->get_custom_titlebar());
assert(window->get_wayland_titlebar());
assert(window->get_settings_session_active());
assert(!window->get_hardware_codec_available());
assert(window->get_portable_service_settings_visible());
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_UI_SNAPSHOT")) {
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
if (!snapshot_written) {
return 2;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_MAIN_INACTIVE_UI_SNAPSHOT")) {
window->set_window_active(false);
window->show();
const bool snapshot_written =
WriteWindowSnapshot(window->window(), snapshot_path);
window->hide();
window->set_window_active(true);
if (!snapshot_written) {
return 3;
}
}
window->set_settings_open(true);
window->set_about_open(true);
assert(window->get_settings_open());
@@ -66,7 +128,37 @@ int main() {
stream->set_tabs(
std::make_shared<slint::VectorModel<crossdesk::ui::StreamTab>>(
std::vector{tab}));
stream->set_custom_titlebar(true);
stream->set_window_maximized(true);
assert(stream->get_tabs()->row_count() == 1);
assert(stream->get_custom_titlebar());
assert(stream->get_window_maximized());
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_maximized(true);
if (!snapshot_written) {
return 4;
}
}
if (const char *snapshot_path =
std::getenv("CROSSDESK_STREAM_INACTIVE_UI_SNAPSHOT")) {
stream->set_window_maximized(false);
stream->set_window_active(false);
stream->show();
const bool snapshot_written =
WriteWindowSnapshot(stream->window(), snapshot_path);
stream->hide();
stream->set_window_active(true);
stream->set_window_maximized(true);
if (!snapshot_written) {
return 5;
}
}
bool tab_reordered = false;
stream->on_reorder_tab([&](int index, float x, float width) {
@@ -75,6 +167,12 @@ int main() {
stream->invoke_reorder_tab(0, 120.0f, 110.0f);
assert(tab_reordered);
bool maximize_requested = false;
stream->on_toggle_maximize_stream_window(
[&] { maximize_requested = true; });
stream->invoke_toggle_maximize_stream_window();
assert(maximize_requested);
crossdesk::ui::FileTransferEntry transfer;
transfer.name = "archive.zip";
transfer.status = "Sending";
+1
View File
@@ -107,6 +107,7 @@ function setup_targets()
add_cxxflags("/bigobj")
end
add_packages("slint")
add_includedirs("src/gui/assets/fonts")
add_rules("slint")
add_files("src/gui/ui/crossdesk_ui.slint")
add_files("tests/slint_ui_smoke_test.cpp")