diff --git a/src/device_controller/device_controller.h b/src/device_controller/device_controller.h index ee41fc7..a17be22 100644 --- a/src/device_controller/device_controller.h +++ b/src/device_controller/device_controller.h @@ -28,6 +28,7 @@ typedef enum { service_status = 5, service_command = 6, keyboard_state = 7, + cursor_state = 8, } ControlType; typedef enum { move = 0, @@ -70,6 +71,47 @@ typedef struct { KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys]; } KeyboardState; +// Keep these values aligned with Slint's MouseCursor enum. The wire protocol +// intentionally carries a semantic cursor instead of a platform handle so a +// Windows, macOS or Linux host can control a different desktop platform. +enum class CursorShape : uint8_t { + default_cursor = 0, + none, + help, + pointer, + progress, + wait, + crosshair, + text, + alias, + copy, + move, + no_drop, + not_allowed, + grab, + grabbing, + col_resize, + row_resize, + n_resize, + e_resize, + s_resize, + w_resize, + ne_resize, + nw_resize, + se_resize, + sw_resize, + ew_resize, + ns_resize, + nesw_resize, + nwse_resize, +}; + +typedef struct { + uint32_t seq; + bool visible; + CursorShape shape; +} CursorState; + typedef struct { char host_name[64]; size_t host_name_size; @@ -96,6 +138,7 @@ struct RemoteAction { Mouse m; Key k; KeyboardState ks; + CursorState cs; HostInfo i; bool a; int d; @@ -141,6 +184,11 @@ struct RemoteAction { j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}}; break; } + case ControlType::cursor_state: + j["cursor_state"] = {{"seq", a.cs.seq}, + {"visible", a.cs.visible}, + {"shape", static_cast(a.cs.shape)}}; + break; case ControlType::audio_capture: j["audio_capture"] = a.a; break; @@ -219,6 +267,18 @@ struct RemoteAction { out.ks.pressed_count = count; break; } + case ControlType::cursor_state: { + const auto& cursor_state_json = j.at("cursor_state"); + const int shape = cursor_state_json.at("shape").get(); + if (shape < static_cast(CursorShape::default_cursor) || + shape > static_cast(CursorShape::nwse_resize)) { + return false; + } + out.cs.seq = cursor_state_json.at("seq").get(); + out.cs.visible = cursor_state_json.at("visible").get(); + out.cs.shape = static_cast(shape); + break; + } case ControlType::audio_capture: out.a = j.at("audio_capture").get(); break; diff --git a/src/gui/application/gui_application.cpp b/src/gui/application/gui_application.cpp index 0018a67..904aa8e 100644 --- a/src/gui/application/gui_application.cpp +++ b/src/gui/application/gui_application.cpp @@ -1796,6 +1796,7 @@ void GuiApplication::Tick() { SyncStreamKeyboardFocus(); #endif devices_.UpdateInteractions(); + ShareLocalCursorState(); UpdateLocalization(); SyncMainWindow(); @@ -1805,6 +1806,58 @@ void GuiApplication::Tick() { SyncServerWindow(); } +void GuiApplication::ShareLocalCursorState() { + constexpr auto kCursorStateHeartbeatInterval = 500ms; + + 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; + }); + } + if (!is_server_mode_ || !peer_ || !has_connected_controller) { + has_shared_cursor_state_ = false; + last_cursor_state_share_time_ = {}; + return; + } + + CursorState sampled{}; + if (!cursor_state_provider_.Sample(&sampled)) { + return; + } + + const auto now = std::chrono::steady_clock::now(); + const bool changed = + !has_shared_cursor_state_ || + sampled.visible != last_shared_cursor_state_.visible || + sampled.shape != last_shared_cursor_state_.shape; + const bool heartbeat_due = + last_cursor_state_share_time_.time_since_epoch().count() == 0 || + now - last_cursor_state_share_time_ >= kCursorStateHeartbeatInterval; + if (!changed && !heartbeat_due) { + return; + } + + sampled.seq = ++cursor_state_sequence_; + RemoteAction action{}; + action.type = ControlType::cursor_state; + action.cs = sampled; + const std::string message = action.to_json(); + const int result = SendDataFrame(peer_, message.c_str(), message.size(), + mouse_label_.c_str()); + if (result != 0) { + LOG_WARN("Send cursor state failed, ret={}", result); + return; + } + + last_shared_cursor_state_ = sampled; + has_shared_cursor_state_ = true; + last_cursor_state_share_time_ = now; +} + void GuiApplication::HandlePasswordChangeResult() { bool succeeded = false; bool uncertain = false; @@ -2364,6 +2417,23 @@ void GuiApplication::SyncStreamWindow() { ? localization::receiving_screen[localization_language_index_] : std::string{})); (*ui_->stream)->set_mouse_control_enabled(props->control_mouse_); + int remote_cursor_shape = + static_cast(CursorShape::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( + props->remote_cursor_state_.visible + ? props->remote_cursor_state_.shape + : CursorShape::none); + } + } + (*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_); #if defined(__APPLE__) fullscreen_button_pressed_ = IsStreamWindowFullscreen(); diff --git a/src/gui/application/gui_application.h b/src/gui/application/gui_application.h index 9aea77c..737eb79 100644 --- a/src/gui/application/gui_application.h +++ b/src/gui/application/gui_application.h @@ -5,6 +5,7 @@ #include #include +#include "runtime/cursor_state_provider.h" #include "runtime/gui_runtime.h" namespace crossdesk { @@ -32,6 +33,7 @@ private: void BindStreamCallbacks(); void BindServerCallbacks(); void Tick(); + void ShareLocalCursorState(); void HandlePasswordChangeResult(); void HandleCredentialRecovery(); void SyncMainWindow(); @@ -70,6 +72,11 @@ private: bool OpenUrl(const std::string &url); std::unique_ptr ui_; + CursorStateProvider cursor_state_provider_; + CursorState last_shared_cursor_state_{}; + bool has_shared_cursor_state_ = false; + uint32_t cursor_state_sequence_ = 0; + std::chrono::steady_clock::time_point last_cursor_state_share_time_{}; std::chrono::steady_clock::time_point next_video_frame_time_{}; #if defined(__linux__) && !defined(__APPLE__) bool use_xwayland_gui_ = false; diff --git a/src/gui/runtime/connection_runtime.cpp b/src/gui/runtime/connection_runtime.cpp index 01f2981..ee48fff 100644 --- a/src/gui/runtime/connection_runtime.cpp +++ b/src/gui/runtime/connection_runtime.cpp @@ -338,6 +338,11 @@ void GuiRuntime::ResetRemoteSessionResources( props->render_rect_dirty_ = true; props->stream_cleanup_pending_ = false; } + { + std::lock_guard lock(props->remote_cursor_state_mutex_); + props->remote_cursor_state_ = {}; + props->remote_cursor_state_received_ = false; + } } std::shared_ptr GuiRuntime::FindRemoteSession( diff --git a/src/gui/runtime/cursor_state_provider.cpp b/src/gui/runtime/cursor_state_provider.cpp new file mode 100644 index 0000000..c55b322 --- /dev/null +++ b/src/gui/runtime/cursor_state_provider.cpp @@ -0,0 +1,183 @@ +#include "runtime/cursor_state_provider.h" + +#if defined(_WIN32) + +#include + +namespace crossdesk { +namespace { + +bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) { + return cursor != nullptr && cursor == LoadCursorW(nullptr, resource); +} + +CursorShape ShapeFromWindowsCursor(HCURSOR cursor) { + if (IsSystemCursor(cursor, IDC_HELP)) return CursorShape::help; + if (IsSystemCursor(cursor, IDC_HAND)) return CursorShape::pointer; + if (IsSystemCursor(cursor, IDC_APPSTARTING)) return CursorShape::progress; + if (IsSystemCursor(cursor, IDC_WAIT)) return CursorShape::wait; + if (IsSystemCursor(cursor, IDC_CROSS)) return CursorShape::crosshair; + if (IsSystemCursor(cursor, IDC_IBEAM)) return CursorShape::text; + if (IsSystemCursor(cursor, IDC_NO)) return CursorShape::not_allowed; + if (IsSystemCursor(cursor, IDC_SIZEALL)) return CursorShape::move; + if (IsSystemCursor(cursor, IDC_SIZEWE)) return CursorShape::ew_resize; + if (IsSystemCursor(cursor, IDC_SIZENS)) return CursorShape::ns_resize; + if (IsSystemCursor(cursor, IDC_SIZENESW)) return CursorShape::nesw_resize; + if (IsSystemCursor(cursor, IDC_SIZENWSE)) return CursorShape::nwse_resize; + if (IsSystemCursor(cursor, IDC_UPARROW)) return CursorShape::n_resize; + return CursorShape::default_cursor; +} + +} // namespace + +struct CursorStateProvider::Impl {}; + +CursorStateProvider::CursorStateProvider() : impl_(std::make_unique()) {} +CursorStateProvider::~CursorStateProvider() = default; + +bool CursorStateProvider::Sample(CursorState* state) { + if (!state) return false; + + CURSORINFO info{}; + info.cbSize = sizeof(info); + if (!GetCursorInfo(&info)) return false; + + state->seq = 0; + state->visible = (info.flags & CURSOR_SHOWING) != 0; + state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor) + : CursorShape::none; + return true; +} + +} // namespace crossdesk + +#elif defined(__linux__) && !defined(__APPLE__) + +#include +#include + +#include +#include +#include + +namespace crossdesk { +namespace { + +std::string Lowercase(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char ch) { return std::tolower(ch); }); + return value; +} + +bool Contains(const std::string& value, const char* token) { + return value.find(token) != std::string::npos; +} + +CursorShape ShapeFromXCursorName(const std::string& raw_name) { + const std::string name = Lowercase(raw_name); + if (Contains(name, "left_ptr_watch") || Contains(name, "progress")) + return CursorShape::progress; + if (Contains(name, "watch") || Contains(name, "wait")) + return CursorShape::wait; + if (Contains(name, "question") || Contains(name, "help")) + return CursorShape::help; + if (Contains(name, "xterm") || Contains(name, "vertical-text") || + name == "text") + return CursorShape::text; + if (Contains(name, "crosshair") || name == "cross" || name == "tcross") + return CursorShape::crosshair; + if (Contains(name, "closedhand") || Contains(name, "grabbing")) + return CursorShape::grabbing; + if (Contains(name, "openhand") || Contains(name, "grab")) + return CursorShape::grab; + if (Contains(name, "dnd-link") || name == "alias") + return CursorShape::alias; + if (Contains(name, "hand") || Contains(name, "pointer") || + Contains(name, "link")) + return CursorShape::pointer; + if (Contains(name, "dnd-copy") || name == "copy") + return CursorShape::copy; + if (Contains(name, "no-drop")) return CursorShape::no_drop; + if (Contains(name, "not-allowed") || Contains(name, "crossed_circle")) + return CursorShape::not_allowed; + if (name == "fleur" || Contains(name, "size_all") || name == "move") + return CursorShape::move; + if (Contains(name, "top_left_corner") || + Contains(name, "bottom_right_corner") || + Contains(name, "nwse-resize") || Contains(name, "size_fdiag")) + return CursorShape::nwse_resize; + if (Contains(name, "top_right_corner") || + Contains(name, "bottom_left_corner") || + Contains(name, "nesw-resize") || Contains(name, "size_bdiag")) + return CursorShape::nesw_resize; + if (Contains(name, "sb_h_double_arrow") || Contains(name, "ew-resize") || + Contains(name, "size_hor")) + return CursorShape::ew_resize; + if (Contains(name, "sb_v_double_arrow") || Contains(name, "ns-resize") || + Contains(name, "size_ver")) + return CursorShape::ns_resize; + if (Contains(name, "col-resize")) return CursorShape::col_resize; + if (Contains(name, "row-resize")) return CursorShape::row_resize; + if (Contains(name, "ne-resize")) return CursorShape::ne_resize; + if (Contains(name, "nw-resize")) return CursorShape::nw_resize; + if (Contains(name, "se-resize")) return CursorShape::se_resize; + if (Contains(name, "sw-resize")) return CursorShape::sw_resize; + if (Contains(name, "top_side") || name == "n-resize") + return CursorShape::n_resize; + if (Contains(name, "right_side") || name == "e-resize") + return CursorShape::e_resize; + if (Contains(name, "bottom_side") || name == "s-resize") + return CursorShape::s_resize; + if (Contains(name, "left_side") || name == "w-resize") + return CursorShape::w_resize; + return CursorShape::default_cursor; +} + +bool CursorHasVisiblePixel(const XFixesCursorImage& image) { + const size_t pixel_count = + static_cast(image.width) * static_cast(image.height); + for (size_t index = 0; index < pixel_count; ++index) { + if (((image.pixels[index] >> 24U) & 0xffU) != 0) return true; + } + return false; +} + +} // namespace + +struct CursorStateProvider::Impl { + Display* display = XOpenDisplay(nullptr); + + ~Impl() { + if (display) XCloseDisplay(display); + } +}; + +CursorStateProvider::CursorStateProvider() : impl_(std::make_unique()) {} +CursorStateProvider::~CursorStateProvider() = default; + +bool CursorStateProvider::Sample(CursorState* state) { + if (!state || !impl_ || !impl_->display) return false; + + XFixesCursorImage* image = XFixesGetCursorImageAndName(impl_->display); + if (!image) return false; + + std::string name; + if (image->atom != None) { + char* atom_name = XGetAtomName(impl_->display, image->atom); + if (atom_name) { + name = atom_name; + XFree(atom_name); + } + } + + state->seq = 0; + state->visible = CursorHasVisiblePixel(*image); + state->shape = state->visible ? ShapeFromXCursorName(name) + : CursorShape::none; + XFree(image); + return true; +} + +} // namespace crossdesk + +#endif diff --git a/src/gui/runtime/cursor_state_provider.h b/src/gui/runtime/cursor_state_provider.h new file mode 100644 index 0000000..35600ae --- /dev/null +++ b/src/gui/runtime/cursor_state_provider.h @@ -0,0 +1,29 @@ +#ifndef CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_ +#define CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_ + +#include + +#include "device_controller.h" + +namespace crossdesk { + +// Samples the cursor that is currently displayed by the controlled desktop +// and converts platform-specific cursor handles to protocol cursor shapes. +class CursorStateProvider { + public: + CursorStateProvider(); + ~CursorStateProvider(); + + CursorStateProvider(const CursorStateProvider&) = delete; + CursorStateProvider& operator=(const CursorStateProvider&) = delete; + + bool Sample(CursorState* state); + + private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace crossdesk + +#endif // CROSSDESK_GUI_CURSOR_STATE_PROVIDER_H_ diff --git a/src/gui/runtime/cursor_state_provider_mac.mm b/src/gui/runtime/cursor_state_provider_mac.mm new file mode 100644 index 0000000..1e61c1b --- /dev/null +++ b/src/gui/runtime/cursor_state_provider_mac.mm @@ -0,0 +1,72 @@ +#include "runtime/cursor_state_provider.h" + +#if defined(__APPLE__) + +#import +#import + +namespace crossdesk { +namespace { + +bool SameCursor(NSCursor* left, NSCursor* right) { + if (left == right || [left isEqual:right]) return true; + return left && right && NSEqualPoints(left.hotSpot, right.hotSpot) && + [left.image isEqual:right.image]; +} + +CursorShape ShapeFromMacCursor(NSCursor* cursor) { + if (SameCursor(cursor, NSCursor.pointingHandCursor)) + return CursorShape::pointer; + if (SameCursor(cursor, NSCursor.crosshairCursor)) + return CursorShape::crosshair; + if (SameCursor(cursor, NSCursor.IBeamCursor) || + SameCursor(cursor, NSCursor.IBeamCursorForVerticalLayout)) + return CursorShape::text; + if (SameCursor(cursor, NSCursor.operationNotAllowedCursor)) + return CursorShape::not_allowed; + if (SameCursor(cursor, NSCursor.dragLinkCursor)) return CursorShape::alias; + if (SameCursor(cursor, NSCursor.dragCopyCursor)) return CursorShape::copy; + if (SameCursor(cursor, NSCursor.openHandCursor)) return CursorShape::grab; + if (SameCursor(cursor, NSCursor.closedHandCursor)) + return CursorShape::grabbing; + if (SameCursor(cursor, NSCursor.resizeLeftRightCursor)) + return CursorShape::ew_resize; + if (SameCursor(cursor, NSCursor.resizeUpDownCursor)) + return CursorShape::ns_resize; + if (SameCursor(cursor, NSCursor.resizeUpCursor)) + return CursorShape::n_resize; + if (SameCursor(cursor, NSCursor.resizeRightCursor)) + return CursorShape::e_resize; + if (SameCursor(cursor, NSCursor.resizeDownCursor)) + return CursorShape::s_resize; + if (SameCursor(cursor, NSCursor.resizeLeftCursor)) + return CursorShape::w_resize; + return CursorShape::default_cursor; +} + +} // namespace + +struct CursorStateProvider::Impl {}; + +CursorStateProvider::CursorStateProvider() : impl_(std::make_unique()) {} +CursorStateProvider::~CursorStateProvider() = default; + +bool CursorStateProvider::Sample(CursorState* state) { + if (!state) return false; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSCursor* cursor = NSCursor.currentSystemCursor; + const bool visible = CGCursorIsVisible(); +#pragma clang diagnostic pop + + state->seq = 0; + state->visible = visible && cursor != nil; + state->shape = state->visible ? ShapeFromMacCursor(cursor) + : CursorShape::none; + return true; +} + +} // namespace crossdesk + +#endif diff --git a/src/gui/runtime/peer_data_callbacks.cpp b/src/gui/runtime/peer_data_callbacks.cpp index 0f162c9..b852270 100644 --- a/src/gui/runtime/peer_data_callbacks.cpp +++ b/src/gui/runtime/peer_data_callbacks.cpp @@ -164,6 +164,31 @@ void PeerEventHandler::OnReceiveDataBuffer( return; } + if (remote_action.type == ControlType::cursor_state) { + std::shared_ptr props; + { + std::shared_lock lock(runtime->remote_sessions_mutex_); + auto props_it = runtime->remote_sessions_.find(remote_id); + if (props_it != runtime->remote_sessions_.end()) { + props = props_it->second; + } + } + if (!props) { + return; + } + + std::lock_guard lock(props->remote_cursor_state_mutex_); + const uint32_t previous_seq = props->remote_cursor_state_.seq; + const bool is_newer = + !props->remote_cursor_state_received_ || + static_cast(remote_action.cs.seq - previous_seq) > 0; + if (is_newer) { + props->remote_cursor_state_ = remote_action.cs; + props->remote_cursor_state_received_ = true; + } + return; + } + if (remote_action.type == ControlType::host_infomation) { bool is_client_mode = false; std::shared_ptr props; diff --git a/src/gui/runtime/peer_event_handler.cpp b/src/gui/runtime/peer_event_handler.cpp index c825518..88be8de 100644 --- a/src/gui/runtime/peer_event_handler.cpp +++ b/src/gui/runtime/peer_event_handler.cpp @@ -249,6 +249,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, switch (status) { case ConnectionStatus::Connected: { runtime->ResetRemoteServiceStatus(*props); + { + std::lock_guard lock(props->remote_cursor_state_mutex_); + props->remote_cursor_state_ = {}; + props->remote_cursor_state_received_ = false; + } { RemoteAction remote_action; remote_action.i.display_num = @@ -313,6 +318,11 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, props->connection_established_ = false; props->enable_mouse_control_ = false; runtime->ResetRemoteServiceStatus(*props); + { + std::lock_guard lock(props->remote_cursor_state_mutex_); + props->remote_cursor_state_ = {}; + props->remote_cursor_state_received_ = false; + } std::shared_ptr> native_snapshot; int native_snapshot_width = 0; diff --git a/src/gui/runtime/remote_session.h b/src/gui/runtime/remote_session.h index 276e2ff..cdb66b7 100644 --- a/src/gui/runtime/remote_session.h +++ b/src/gui/runtime/remote_session.h @@ -13,6 +13,7 @@ #include #include +#include "device_controller.h" #include "display_info.h" #include "minirtc.h" @@ -148,6 +149,12 @@ struct RemoteSession { bool remote_service_available_ = false; std::string remote_interactive_stage_; std::vector display_info_list_; + // Cursor snapshots arrive on the transport callback thread and are applied + // by the Slint UI thread. Keep the snapshot atomic as a unit so visibility + // and shape cannot briefly come from different protocol messages. + std::mutex remote_cursor_state_mutex_; + CursorState remote_cursor_state_{}; + bool remote_cursor_state_received_ = false; // Shared by minirtc callbacks, Slint rendering and SDL audio callbacks. std::atomic connection_status_ = ConnectionStatus::Closed; TraversalMode traversal_mode_ = TraversalMode::UnknownMode; diff --git a/src/gui/ui/stream_window.slint b/src/gui/ui/stream_window.slint index 8bf35e2..d5347f4 100644 --- a/src/gui/ui/stream_window.slint +++ b/src/gui/ui/stream_window.slint @@ -133,6 +133,9 @@ export component StreamWindow inherits Window { in property <[string]> displays; in-out property selected-display: 0; in property mouse-control-enabled: true; + // Numeric values mirror crossdesk::CursorShape and Slint's MouseCursor. + in property remote-cursor-active: false; + in property remote-cursor-shape: 0; in property audio-enabled: true; in property srtp-enabled: false; in property fullscreen-enabled: false; @@ -551,6 +554,36 @@ export component StreamWindow inherits Window { accept } pointer := TouchArea { + mouse-cursor: !root.remote-cursor-active ? MouseCursor.default + : root.remote-cursor-shape == 1 ? MouseCursor.none + : root.remote-cursor-shape == 2 ? MouseCursor.help + : root.remote-cursor-shape == 3 ? MouseCursor.pointer + : root.remote-cursor-shape == 4 ? MouseCursor.progress + : root.remote-cursor-shape == 5 ? MouseCursor.wait + : root.remote-cursor-shape == 6 ? MouseCursor.crosshair + : root.remote-cursor-shape == 7 ? MouseCursor.text + : root.remote-cursor-shape == 8 ? MouseCursor.alias + : root.remote-cursor-shape == 9 ? MouseCursor.copy + : root.remote-cursor-shape == 10 ? MouseCursor.move + : root.remote-cursor-shape == 11 ? MouseCursor.no-drop + : root.remote-cursor-shape == 12 ? MouseCursor.not-allowed + : root.remote-cursor-shape == 13 ? MouseCursor.grab + : root.remote-cursor-shape == 14 ? MouseCursor.grabbing + : root.remote-cursor-shape == 15 ? MouseCursor.col-resize + : root.remote-cursor-shape == 16 ? MouseCursor.row-resize + : root.remote-cursor-shape == 17 ? MouseCursor.n-resize + : root.remote-cursor-shape == 18 ? MouseCursor.e-resize + : root.remote-cursor-shape == 19 ? MouseCursor.s-resize + : root.remote-cursor-shape == 20 ? MouseCursor.w-resize + : root.remote-cursor-shape == 21 ? MouseCursor.ne-resize + : root.remote-cursor-shape == 22 ? MouseCursor.nw-resize + : root.remote-cursor-shape == 23 ? MouseCursor.se-resize + : root.remote-cursor-shape == 24 ? MouseCursor.sw-resize + : root.remote-cursor-shape == 25 ? MouseCursor.ew-resize + : root.remote-cursor-shape == 26 ? MouseCursor.ns-resize + : root.remote-cursor-shape == 27 ? MouseCursor.nesw-resize + : root.remote-cursor-shape == 28 ? MouseCursor.nwse-resize + : MouseCursor.default; pointer-event(event) => { let over-control = self.mouse-x >= control.x && self.mouse-x <= control.x + control.width diff --git a/xmake/platform.lua b/xmake/platform.lua index f0681c6..7183b91 100644 --- a/xmake/platform.lua +++ b/xmake/platform.lua @@ -84,7 +84,8 @@ function setup_platform_settings() add_ldflags("-Wl,-ld_classic") end add_cxflags("-Wno-unused-variable") - add_frameworks("Cocoa", "Metal", "QuartzCore", "IOSurface", + add_frameworks("Cocoa", "CoreGraphics", "Metal", "QuartzCore", + "IOSurface", "ScreenCaptureKit", "AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox") end