diff --git a/src/gui/application/gui_application.cpp b/src/gui/application/gui_application.cpp index 3896f59..f9c939d 100644 --- a/src/gui/application/gui_application.cpp +++ b/src/gui/application/gui_application.cpp @@ -681,6 +681,7 @@ struct GuiApplication::SlintUi { bool portable_service_dialog_initialized = false; std::string recent_model_signature; slint::Timer timer; + WindowDragState main_drag; WindowDragState server_drag; int main_native_titlebar_attempts = 30; int stream_live_resize_configuration_attempts = 0; @@ -816,6 +817,9 @@ void GuiApplication::InitializeModules() { void GuiApplication::InitializeUi() { ui_ = std::make_unique(); +#if _WIN32 + ui_->main->set_custom_titlebar(true); +#endif std::vector release_note_blocks; for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) { @@ -1109,6 +1113,27 @@ void GuiApplication::BindMainCallbacks() { exit_ = true; 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; + } + if (MinimizeMainWindowToTray()) { + ui_->main->hide(); + return; + } + exit_ = true; + ui_->main->hide(); + }); main->on_copy_local_id([this] { if (!SDL_SetClipboardText(client_id_)) { LOG_WARN("Copy local id failed: {}", SDL_GetError()); diff --git a/src/gui/ui/main_window.slint b/src/gui/ui/main_window.slint index 5f6a6df..969c824 100644 --- a/src/gui/ui/main_window.slint +++ b/src/gui/ui/main_window.slint @@ -184,15 +184,16 @@ component ConnectionDialogButton inherits Rectangle { export component MainWindow inherits Window { title: "CrossDesk"; preferred-width: 640px; - preferred-height: 450px; + preferred-height: root.custom-titlebar ? 481px : 450px; min-width: 640px; max-width: 640px; - min-height: 450px; - max-height: 450px; - no-frame: false; + min-height: root.custom-titlebar ? 481px : 450px; + max-height: root.custom-titlebar ? 481px : 450px; + no-frame: root.custom-titlebar; background: white; default-font-size: ImGuiFontStyle.base; + in property custom-titlebar: false; in property local-id: ""; in property local-password: ""; in-out property remote-id-input: ""; @@ -270,6 +271,9 @@ export component MainWindow inherits Window { callback cancel-portable-service; callback acknowledge-portable-service; callback acknowledge-portable-service-suppressed; + callback main-title-drag(int, length, length); + callback minimize-main-window; + callback close-main-window; private property menu-open: false; in-out property settings-open: false; @@ -306,7 +310,7 @@ export component MainWindow inherits Window { ? root.recent-tooltip-pointer-x - root.recent-tooltip-width - 12px : root.recent-tooltip-pointer-x + 12px)); private property recent-tooltip-y: min( - root.height - root.recent-tooltip-height - 8px, + root.height - (root.custom-titlebar ? 31px : 0px) - root.recent-tooltip-height - 8px, max(8px, root.recent-tooltip-pointer-y - root.recent-tooltip-height - 8px)); reset-remote-id => { @@ -314,9 +318,100 @@ export component MainWindow inherits Window { remote-id-editor.focus(); } - Rectangle { + if root.custom-titlebar: Rectangle { + x: 0px; + y: 0px; width: root.width; - height: root.height; + height: 31px; + background: #f9f9f9; + z: 100; + + Image { + x: 8px; + y: 7px; + width: 16px; + height: 16px; + source: @image-url("../../../icons/linux/crossdesk_16x16.png"); + image-fit: contain; + } + Text { + x: 30px; + width: parent.width - 122px; + height: parent.height; + text: "CrossDesk"; + color: #202124; + font-size: 12px; + vertical-alignment: center; + overflow: elide; + } + title-drag := TouchArea { + x: 0px; + width: parent.width - 92px; + property 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); } } + } + minimize-button := Rectangle { + x: parent.width - 92px; + width: 46px; + height: parent.height; + background: minimize-touch.pressed ? #c7c7c7 + : minimize-touch.has-hover ? #e5e5e5 + : transparent; + accessible-role: button; + accessible-label: "Minimize"; + accessible-action-default => { minimize-touch.clicked(); } + Text { + text: FontAwesomeIcons.minus; + color: #202124; + font-family: "Font Awesome 6 Free"; + font-weight: 900; + font-size: 10px; + horizontal-alignment: center; + vertical-alignment: center; + accessible-role: none; + } + minimize-touch := TouchArea { clicked => { root.minimize-main-window(); } } + } + close-button := Rectangle { + x: parent.width - 46px; + width: 46px; + height: parent.height; + background: close-touch.pressed ? #b3251b + : close-touch.has-hover ? #c42b1c + : transparent; + accessible-role: button; + accessible-label: "Close"; + accessible-action-default => { close-touch.clicked(); } + Text { + text: FontAwesomeIcons.xmark; + color: close-touch.has-hover || close-touch.pressed ? white : #202124; + font-family: "Font Awesome 6 Free"; + font-weight: 900; + font-size: 12px; + horizontal-alignment: center; + vertical-alignment: center; + accessible-role: none; + } + close-touch := TouchArea { clicked => { root.close-main-window(); } } + } + Rectangle { + y: parent.height - 1px; + height: 1px; + background: #d4d4d4; + } + } + + content-layer := Rectangle { + x: 0px; + y: root.custom-titlebar ? 31px : 0px; + width: root.width; + height: root.height - self.y; + clip: true; + + Rectangle { + width: parent.width; + height: parent.height; background: white; border-width: 0px; border-radius: 0px; @@ -336,8 +431,8 @@ export component MainWindow inherits Window { z: 7; } - // App actions live in the content header; native window controls and - // dragging are provided by the system title bar. + // App actions live in the content header; window controls and dragging + // remain in the native or Windows-specific title bar above it. menu-button := IconButton { x: parent.width - 40px; y: 4px; @@ -782,8 +877,8 @@ export component MainWindow inherits Window { if root.menu-open: TouchArea { x: 0px; y: 0px; - width: root.width; - height: root.height; + width: parent.width; + height: parent.height; z: 9; clicked => { root.menu-open = false; @@ -1509,4 +1604,16 @@ export component MainWindow inherits Window { } } } + } + + if root.custom-titlebar: Rectangle { + x: 0px; + y: 0px; + width: root.width; + height: root.height; + background: transparent; + border-width: 1px; + border-color: #8b8b8b; + z: 200; + } } diff --git a/tests/slint_ui_smoke_test.cpp b/tests/slint_ui_smoke_test.cpp index 5c3cbba..0b529a7 100644 --- a/tests/slint_ui_smoke_test.cpp +++ b/tests/slint_ui_smoke_test.cpp @@ -13,12 +13,14 @@ int main() { window->set_local_password("123456"); window->set_connection_dialog_open(true); window->set_connection_password_required(true); + window->set_custom_titlebar(true); window->set_settings_session_active(true); window->set_hardware_codec_available(false); window->set_portable_service_settings_visible(true); assert(std::string(window->get_local_id()) == "123 456 789"); assert(window->get_connection_dialog_open()); assert(window->get_connection_password_required()); + assert(window->get_custom_titlebar()); assert(window->get_settings_session_active()); assert(!window->get_hardware_codec_available()); assert(window->get_portable_service_settings_visible());