Skip to content

Commit ee543ad

Browse files
authored
Merge pull request #83 from SLM-Audio/syl/mouse-manipulation
Add cursor manipulation functions
2 parents b744b49 + ae68a09 commit ee543ad

File tree

9 files changed

+209
-4
lines changed

9 files changed

+209
-4
lines changed

include/mostly_harmless/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ set(MOSTLYHARMLESS_HEADERS
1919
${CMAKE_CURRENT_SOURCE_DIR}/mostlyharmless_Parameters.h
2020
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewBase.h
2121
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewEditor.h
22+
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_Cursor.h
2223
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_Colour.h
2324
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TaskThread.h
2425
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Timer.h
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// Created by Syl Morrison on 17/01/2026.
3+
//
4+
5+
#ifndef MOSTLYHARMLESS_CURSOR_H
6+
#define MOSTLYHARMLESS_CURSOR_H
7+
#include <cstdint>
8+
namespace mostly_harmless::gui::cursor {
9+
/**
10+
* Shows or hides the cursor
11+
* @param show Whether the cursor should be shown or hidden.
12+
*/
13+
auto setCursorState(bool show) -> void;
14+
15+
/**
16+
* Retrieves the screen x & y for the cursor, and stores the results in x and y.
17+
* @param x A pointer to a var to store the cursor's x position in.
18+
* @param y A pointer to a var to store the cursor's y position in.
19+
*/
20+
auto getCursorPosition(std::uint32_t* x, std::uint32_t* y) -> void;
21+
22+
/**
23+
* Sets the cursor's position to the specified (screen) x & y coords.
24+
* @param x The new x position for the cursor (in screen bounds).
25+
* @param y The new y position for the cursor (in screen bounds).
26+
*/
27+
auto setCursorPosition(std::uint32_t x, std::uint32_t y) -> void;
28+
} // namespace mostly_harmless::gui::cursor
29+
#endif // MOSTLYHARMLESS_CURSOR_H

include/mostly_harmless/gui/mostlyharmless_WebviewEditor.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace mostly_harmless::gui {
2222
*
2323
* To use it, you'll probably want to still subclass it, but the boilerplate will be fairly minimal. See WebviewBase for more fine grained details.
2424
*
25-
* The default implementation establishes bindings to 3 javascript functions, pertaining to parameter updates. These are:
25+
* The default implementation establishes bindings to several javascript functions. Firstly the ones pertaining to parameter updates:
2626
* `beginParamGesture()`, `setParamValue()`, and `endParamGesture()`. \n
2727
* Each of these functions take an object as an arg, expected to be formatted as json containing the paramId to affect, and the value to set.
2828
* For example:
@@ -41,6 +41,18 @@ namespace mostly_harmless::gui {
4141
* The default implementation will attempt to parse the args (and assert fail if it failed),
4242
* and then enqueue the param changes to the guiToProcQueue, for the host and audio side to pick up.
4343
*
44+
* For what I've been calling "ouroborosing" the cursor position, and generally providing the facilities to hide the cursor, snap back to original mouse down position on a drag etc, we also provide some extra helpers here.
45+
* `beginScopedCursorMoveGesture()` caches the mouse down position at the time of calling, and hides the cursor.
46+
* `endScopedCursorMoveGesture()` restores the cached cursor position, and shows the cursor. These functions should be called as a pair,
47+
* for a slider in mouseDown / mouseUp for example.
48+
*
49+
* `resetCursorPosition()` does much the same as `endScopedCursorMoveGesture()`, except it doesn't zero any of the internally cached variables, and doesn't show the cursor.
50+
* The use case is quite different, and is for forcing the cursor to be within a given area - this allows for "infinite drag", or "ouroborosing" without the cursor hitting the screen edges.
51+
* If `resetCursorPosition()` is used, then to avoid unexpected jumps, use `tickCursorMove()`, and use its return value instead of the js side `event.movementY` variable.
52+
* It updates the internal cache of the last mouse position, and then returns the difference between the old and new values. Call it every time `mousemove` is called for example.
53+
* `clearPreviousCursorPosition()` in this paradigm should be called on mouseUp, and simply nulls the last mouse position stored in the cache, preparing it for a new gesture.
54+
*
55+
*
4456
*
4557
* The structure of an event in the default implementation is:
4658
*
@@ -158,6 +170,11 @@ namespace mostly_harmless::gui {
158170
virtual choc::value::Value endParamChangeGestureCallback(const choc::value::ValueView& args);
159171

160172
core::ISharedState* m_sharedState{ nullptr };
173+
174+
struct {
175+
std::optional<std::pair<std::uint32_t, std::uint32_t>> lastMouseDownLocation{};
176+
std::optional<std::pair<std::uint32_t, std::uint32_t>> lastMousePosition{};
177+
} m_cursorState;
161178
};
162179
} // namespace mostly_harmless::gui
163180
#endif // MOSTLYHARMLESS_MOSTLYHARMLESS_WEBVIEWEDITOR_H

include/mostly_harmless/gui/platform/mostlyharmless_GuiHelpersMacOS.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#ifndef MOSTLYHARMLESS_MOSTLYHARMLESS_GUIHELPERSMACOS_H
66
#define MOSTLYHARMLESS_MOSTLYHARMLESS_GUIHELPERSMACOS_H
7+
#include <mostly_harmless/gui/mostlyharmless_Cursor.h>
78
#include <mostly_harmless/gui/mostlyharmless_Colour.h>
89
#include <cstdint>
910
namespace mostly_harmless::gui::helpers::macos {
@@ -43,5 +44,32 @@ namespace mostly_harmless::gui::helpers::macos {
4344
* \param viewHandle A void* to the NSView to set hidden.
4445
*/
4546
void hideView(void* viewHandle);
47+
48+
/**
49+
* Retrieves the current mouse position in screen coords, and stores them in `x` and `y`.
50+
* @param x A pointer to your x variable - the result will be written here.
51+
* @param y A pointer to your y variable - the result will be written here.
52+
*/
53+
void getMousePos(std::uint32_t* x, std::uint32_t* y);
54+
55+
/**
56+
* Sets the mouse position to the provided position (in screen coords).
57+
* Internally handles translating from screen coords to global space coords.
58+
* @param newX The x position to set the mouse position to (in screen space).
59+
* @param newY The y position to set the mouse position to (in screen space).
60+
*/
61+
void setMousePos(std::uint32_t newX, std::uint32_t newY);
62+
63+
/**
64+
* Shows (or more accurately, "unhides") the cursor.
65+
*/
66+
void showCursor();
67+
68+
/**
69+
* Hides the cursor.
70+
*/
71+
void hideCursor();
72+
73+
4674
} // namespace mostly_harmless::gui::helpers::macos
4775
#endif // MOSTLYHARMLESS_MOSTLYHARMLESS_GUIHELPERSMACOS_H

source/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ set(MOSTLYHARMLESS_SOURCES
2626
${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_MidiEvent.cpp
2727
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewBase.cpp
2828
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewEditor.cpp
29+
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_Cursor.cpp
2930
${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_Colour.cpp
3031
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TaskThread.cpp
3132
${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Timer.cpp
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// Created by Syl Morrison on 17/01/2026.
3+
//
4+
5+
#if defined(MOSTLY_HARMLESS_WINDOWS)
6+
#include <Windows.h>
7+
#elif defined(MOSTLY_HARMLESS_MACOS)
8+
#include <mostly_harmless/gui/platform/mostlyharmless_GuiHelpersMacOS.h>
9+
#endif
10+
#include <mostly_harmless/gui/mostlyharmless_Cursor.h>
11+
namespace mostly_harmless::gui::cursor {
12+
#if defined(MOSTLY_HARMLESS_WINDOWS)
13+
auto setCursorState(bool show) -> void {
14+
::ShowCursor(show ? SW_SHOW : SW_HIDE);
15+
}
16+
17+
auto getCursorPosition(std::uint32_t* x, std::uint32_t* y) -> void {
18+
::POINT cursorPos;
19+
::GetCursorPos(&cursorPos);
20+
*x = static_cast<std::uint32_t>(cursorPos.x);
21+
*y = static_cast<std::uint32_t>(cursorPos.y);
22+
}
23+
24+
auto setCursorPosition(std::uint32_t x, std::uint32_t y) -> void {
25+
::SetCursorPos(static_cast<::LONG>(x), static_cast<::LONG>(y));
26+
}
27+
28+
#elif defined(MOSTLY_HARMLESS_MACOS)
29+
auto setCursorState(bool show) -> void {
30+
if (show) {
31+
gui::helpers::macos::showCursor();
32+
} else {
33+
gui::helpers::macos::hideCursor();
34+
}
35+
}
36+
37+
auto getCursorPosition(std::uint32_t* x, std::uint32_t* y) -> void {
38+
gui::helpers::macos::getMousePos(x, y);
39+
}
40+
41+
auto setCursorPosition(std::uint32_t x, std::uint32_t y) -> void {
42+
gui::helpers::macos::setMousePos(x, y);
43+
}
44+
45+
#else
46+
static_assert(false);
47+
#endif
48+
49+
50+
} // namespace mostly_harmless::gui::cursor

source/gui/mostlyharmless_WebviewBase.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,5 +280,4 @@ namespace mostly_harmless::gui {
280280
m_impl->hide();
281281
}
282282

283-
284283
} // namespace mostly_harmless::gui

source/gui/mostlyharmless_WebviewEditor.cpp

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
//
22
// Created by Syl Morrison on 11/08/2024.
33
//
4-
#include <choc/gui/choc_DesktopWindow.h>
5-
#include <choc/gui/choc_WebView.h>
64
#include <mostly_harmless/gui/mostlyharmless_WebviewEditor.h>
5+
#include "mostly_harmless/utils/mostlyharmless_OnScopeExit.h"
76
#include <mostly_harmless/utils/mostlyharmless_Macros.h>
87
#if defined(MOSTLY_HARMLESS_MACOS)
98
#include <mostly_harmless/gui/platform/mostlyharmless_GuiHelpersMacOS.h>
109
#elif defined(MOSTLY_HARMLESS_WINDOWS)
1110
#include <windef.h>
1211
#include <winuser.h>
1312
#endif
13+
#include <choc/gui/choc_DesktopWindow.h>
14+
#include <choc/gui/choc_WebView.h>
1415
#include <cassert>
1516
#include <filesystem>
17+
#include <mostly_harmless/gui/mostlyharmless_Cursor.h>
1618
namespace mostly_harmless::gui {
1719

1820
WebviewEditor::WebviewEditor(core::ISharedState* sharedState, std::uint32_t initialWidth, std::uint32_t initialHeight, Colour backgroundColour) : WebviewBase(initialWidth,
@@ -23,6 +25,7 @@ namespace mostly_harmless::gui {
2325

2426
void WebviewEditor::initialise() {
2527
WebviewBase::initialise();
28+
2629
auto beginParamGestureCallback_ = [this](const choc::value::ValueView& args) -> choc::value::Value {
2730
return beginParamChangeGestureCallback(args);
2831
};
@@ -34,9 +37,60 @@ namespace mostly_harmless::gui {
3437
auto endParamGestureCallback_ = [this](const choc::value::ValueView& args) -> choc::value::Value {
3538
return endParamChangeGestureCallback(args);
3639
};
40+
41+
auto beginScopedCursorMoveGestureCallback_ = [this](const choc::value::ValueView& args) -> choc::value::Value {
42+
std::uint32_t x, y;
43+
cursor::getCursorPosition(&x, &y);
44+
m_cursorState.lastMouseDownLocation = std::make_pair(x, y);
45+
cursor::setCursorState(false);
46+
return {};
47+
};
48+
49+
auto endScopedCursorMoveGestureCallback_ = [this](const choc::value::ValueView& args) -> choc::value::Value {
50+
mostly_harmless::utils::OnScopeExit se{ [this]() -> void {
51+
cursor::setCursorState(true);
52+
} };
53+
if (!m_cursorState.lastMouseDownLocation) {
54+
return {};
55+
}
56+
const auto [x, y] = *m_cursorState.lastMouseDownLocation;
57+
cursor::setCursorPosition(x, y);
58+
m_cursorState.lastMouseDownLocation = {};
59+
return {};
60+
};
61+
62+
auto resetCursorPositionCallback_ = [this](const choc::value::ValueView& args) -> choc::value::Value {
63+
const auto [x, y] = m_cursorState.lastMouseDownLocation.value_or(std::make_pair(0, 0));
64+
mostly_harmless::gui::cursor::setCursorPosition(x, y);
65+
m_cursorState.lastMousePosition = std::make_pair(x, y);
66+
return {};
67+
};
68+
69+
auto tickCursorMoveCallback_ = [this](const choc::value::ValueView& args) -> choc::value::Value {
70+
const auto lastMouseDownLocation = m_cursorState.lastMouseDownLocation.value_or(std::make_pair(0, 0));
71+
const auto [prevX, prevY] = m_cursorState.lastMousePosition.value_or(lastMouseDownLocation);
72+
std::uint32_t x, y;
73+
mostly_harmless::gui::cursor::getCursorPosition(&x, &y);
74+
m_cursorState.lastMousePosition = std::make_pair(x, y);
75+
const auto deltaX = static_cast<std::int32_t>(x) - static_cast<std::int32_t>(prevX);
76+
const auto deltaY = static_cast<std::int32_t>(y) - static_cast<std::int32_t>(prevY);
77+
const auto res = choc::json::create("x", deltaX, "y", deltaY);
78+
return res;
79+
};
80+
81+
auto clearPreviousCursorPositionCallback_ = [this](const choc::value::ValueView& args) -> choc::value::Value {
82+
m_cursorState.lastMousePosition = {};
83+
return {};
84+
};
85+
3786
m_internalWebview->bind("beginParamGesture", std::move(beginParamGestureCallback_));
3887
m_internalWebview->bind("setParamValue", std::move(paramChangeCallback_));
3988
m_internalWebview->bind("endParamGesture", std::move(endParamGestureCallback_));
89+
m_internalWebview->bind("beginScopedCursorMoveGesture", std::move(beginScopedCursorMoveGestureCallback_));
90+
m_internalWebview->bind("endScopedCursorMoveGesture", std::move(endScopedCursorMoveGestureCallback_));
91+
m_internalWebview->bind("resetCursorPosition", std::move(resetCursorPositionCallback_));
92+
m_internalWebview->bind("tickCursorMove", std::move(tickCursorMoveCallback_));
93+
m_internalWebview->bind("clearPreviousCursorPosition", std::move(clearPreviousCursorPositionCallback_));
4094
}
4195

4296
bool WebviewEditor::allowResize() const noexcept {

source/gui/platform/mostlyharmless_GuiHelpersMacOS.mm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,30 @@ void hideView(void* viewHandle) {
6868
auto* asView = static_cast<NSView*>(viewHandle);
6969
asView.hidden = true;
7070
}
71+
72+
void getMousePos(std::uint32_t* x, std::uint32_t* y) {
73+
const auto mouseLocation = [NSEvent mouseLocation];
74+
*x = static_cast<std::uint32_t>(mouseLocation.x);
75+
*y = static_cast<std::uint32_t>(mouseLocation.y);
76+
}
77+
78+
void setMousePos(std::uint32_t newX, std::uint32_t newY) {
79+
const auto frame = [[NSScreen mainScreen] frame];
80+
const auto h = frame.size.height;
81+
const auto translatedY = h - newY;
82+
const auto pt = CGPointMake(newX, translatedY);
83+
// Note that while this doesn't generate an event, its delta will be added to the next mouse event that *does* generate an event - so in effect, this can cause a massive fucking jump -
84+
// If you can intercept the next event, this is workaroundable - see WebviewEditor's ouroboros stuff
85+
CGWarpMouseCursorPosition(pt);
86+
CGAssociateMouseAndMouseCursorPosition(true);
87+
}
88+
89+
void showCursor() {
90+
[NSCursor unhide];
91+
}
92+
93+
void hideCursor() {
94+
[NSCursor hide];
95+
}
96+
7197
} // namespace mostly_harmless::gui::helpers::macos

0 commit comments

Comments
 (0)