diff --git a/examples/delay/source/PluginEditor.cpp b/examples/delay/source/PluginEditor.cpp index 473b0b0..7eb22a6 100644 --- a/examples/delay/source/PluginEditor.cpp +++ b/examples/delay/source/PluginEditor.cpp @@ -6,7 +6,7 @@ #include "Parameters.h" #include namespace examples::delay { - PluginEditor::PluginEditor(std::uint32_t width, std::uint32_t height) : mostly_harmless::gui::WebviewEditor(width, height) { + PluginEditor::PluginEditor(std::uint32_t width, std::uint32_t height) : mostly_harmless::gui::WebviewEditor(width, height, mostly_harmless::gui::Colour{ 0xFF89CC04 }) { } void PluginEditor::initialise(mostly_harmless::gui::EditorContext context) { diff --git a/examples/gain/CMakeLists.txt b/examples/gain/CMakeLists.txt index 76735db..6e986ae 100644 --- a/examples/gain/CMakeLists.txt +++ b/examples/gain/CMakeLists.txt @@ -55,7 +55,7 @@ else () endif () add_custom_target( - Vite + gain-vite ALL DEPENDS ${GAIN_GUI_OUTPUT} ) @@ -64,7 +64,7 @@ mostly_harmless_add_binary_data(Gain ROOT ${PROJECT_BINARY_DIR}/gain-gui BINARY_SOURCES ${GAIN_GUI_OUTPUT} ) -add_dependencies(WebResources Vite) +add_dependencies(WebResources gain-vite) target_sources(Gain PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/source/Gain.cpp diff --git a/examples/gain/source/GainEditor.cpp b/examples/gain/source/GainEditor.cpp index 365eff9..17b1148 100644 --- a/examples/gain/source/GainEditor.cpp +++ b/examples/gain/source/GainEditor.cpp @@ -11,7 +11,7 @@ #include namespace examples::gain { - [[nodiscard]] Resource createResourceFor(const std::string& name) { + [[nodiscard]] mostly_harmless::gui::WebviewEditor::Resource createResourceFor(const std::string& name) { auto resOpt = mostly_harmless::WebResources::getNamedResource(name); assert(resOpt); auto [data, size] = *resOpt; @@ -19,17 +19,17 @@ namespace examples::gain { assert(mimeType); // Evil evil evil evil evil evil (but safe I think) auto* asUnsigned = reinterpret_cast(data); - Resource temp; + mostly_harmless::gui::WebviewEditor::Resource temp; temp.data = { asUnsigned, asUnsigned + size }; temp.mimeType = *mimeType; return temp; } - GainEditor::GainEditor(std::uint32_t width, std::uint32_t height) : mostly_harmless::gui::WebviewEditor(width, height) { + GainEditor::GainEditor(std::uint32_t width, std::uint32_t height) : mostly_harmless::gui::WebviewEditor(width, height, mostly_harmless::gui::Colour(0xFF89CC04)) { m_resources.emplace("/index.html", createResourceFor("index.html")); m_resources.emplace("/index.css", createResourceFor("index.css")); m_resources.emplace("/index.js", createResourceFor("index.js")); - auto fetchResourceCallback = [this](const std::string& url) -> std::optional { + auto contentProvider = [this](const std::string& url) -> std::optional { const auto requested = url == "/" ? "/index.html" : url; const auto it = m_resources.find(requested); if (it == m_resources.end()) return {}; @@ -46,9 +46,9 @@ namespace examples::gain { } initialDataStream << "};"; #if defined(GAIN_HOT_RELOAD) - this->setOptions({ .enableDebugMode = true, .initScript = initialDataStream.str() }); + this->setOptions({ .enableDebug = true, .initScript = initialDataStream.str() }); #else - this->setOptions({ .enableDebugMode = true, .fetchResource = std::move(fetchResourceCallback), .initScript = initialDataStream.str() }); + this->setOptions({ .enableDebug = true, .contentProvider = std::move(contentProvider), .initScript = initialDataStream.str() }); #endif } diff --git a/examples/gain/source/GainEditor.h b/examples/gain/source/GainEditor.h index c993e7a..c40aa32 100644 --- a/examples/gain/source/GainEditor.h +++ b/examples/gain/source/GainEditor.h @@ -8,7 +8,6 @@ #include "Parameters.h" #include namespace examples::gain { - using Resource = choc::ui::WebView::Options::Resource; class GainEditor : public mostly_harmless::gui::WebviewEditor { public: GainEditor(std::uint32_t width, std::uint32_t height); @@ -18,7 +17,7 @@ namespace examples::gain { private: // Usually I'd have this as a static in the TU, but because the underlying map `getNamedResource()` queries is also static, it leads to weird issues with init-order // If I find a way to guarantee the order, I'll probably switch to that approach instead, but for now this will have to do - std::unordered_map m_resources; + std::unordered_map m_resources; }; } // namespace examples::gain diff --git a/include/mostly_harmless/CMakeLists.txt b/include/mostly_harmless/CMakeLists.txt index 19a214c..45244ea 100644 --- a/include/mostly_harmless/CMakeLists.txt +++ b/include/mostly_harmless/CMakeLists.txt @@ -12,6 +12,7 @@ set(MOSTLYHARMLESS_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_EditorContext.h ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_IEditor.h ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewEditor.h + ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_Colour.h ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TaskThread.h ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Timer.h ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_ParamEvent.h diff --git a/include/mostly_harmless/gui/mostlyharmless_Colour.h b/include/mostly_harmless/gui/mostlyharmless_Colour.h new file mode 100644 index 0000000..dc27f73 --- /dev/null +++ b/include/mostly_harmless/gui/mostlyharmless_Colour.h @@ -0,0 +1,36 @@ +// +// Created by Syl on 08/09/2024. +// + +#ifndef MOSTLY_HARMLESS_COLOUR_H +#define MOSTLY_HARMLESS_COLOUR_H +#include +namespace mostly_harmless::gui { + /** + * \brief Convenience struct representing a colour. + */ + struct Colour final { + /** + * Constructs a colour from an ARGB hex colour, in the format `0xAARRGGBB` - for example, fully opaque red would be `0xFFFF0000` + * \param argb The hex code for the desired colour. + */ + explicit Colour(std::uint32_t argb); + /** + * Constructs a colour from individual (0 to 255) rgb args. In this overload, alpha defaults to 255. + * \param r_ The 0 to 255 value for red. + * \param g_ The 0 to 255 value for green. + * \param b_ The 0 to 255 value for blue. + */ + Colour(std::uint8_t r_, std::uint8_t g_, std::uint8_t b_); + /** + * Constructs a colour from individual (0 to 255) argb args. + * \param a_ The 0 to 255 value for alpha (255 being fully opaque) + * \param r_ The 0 to 255 value for red. + * \param g_ The 0 to 255 value for green. + * \param b_ The 0 to 255 value for blue. + */ + Colour(std::uint8_t a_, std::uint8_t r_, std::uint8_t g_, std::uint8_t b_); + std::uint8_t a, r, g, b; + }; +} // namespace mostly_harmless::gui +#endif // MOSTLY_HARMLESS_COLOUR_H diff --git a/include/mostly_harmless/gui/mostlyharmless_WebviewEditor.h b/include/mostly_harmless/gui/mostlyharmless_WebviewEditor.h index 47e6adb..b84a34d 100644 --- a/include/mostly_harmless/gui/mostlyharmless_WebviewEditor.h +++ b/include/mostly_harmless/gui/mostlyharmless_WebviewEditor.h @@ -5,6 +5,7 @@ #ifndef MOSTLYHARMLESS_MOSTLYHARMLESS_WEBVIEWEDITOR_H #define MOSTLYHARMLESS_MOSTLYHARMLESS_WEBVIEWEDITOR_H #include +#include #include #include #include @@ -22,23 +23,85 @@ namespace mostly_harmless::gui { * * Provides the basic setup code for the Webview, but you'll likely want to derive from this, and override some of its functions. * The underlying `choc::ui::WebView` can be accessed via the protected `m_internalWebview` member. + * If you don't call `setOptions` before initialise is called, then the internal webview will be constructed with some default options, namely: + * + * ``` + * enableDebug = false + * transparentBackground = true + * ``` + * */ class WebviewEditor : public IEditor { public: + /** + * \brief Tiny container struct for web resources. + * + * If serving from RAM, your editor should hold an internal map of `route:Resource`, populated in your constructor.
+ * MIME types can be retrieved with mostly_harmless::gui::getMimeType(). + */ + struct Resource { + Resource() = default; + /** + * Constructs a Resource from a char[] and a mime type. + * \param content A char[] containing the data for this resource. + * \param mimeType_ The associated MIME type for this resource. + */ + Resource(std::string_view content, std::string mimeType_); + /** + * The binary data for this resource. + */ + std::vector data; + /** + * The associated MIME type for this resource. + */ + std::string mimeType; + }; + + /** + * \brief Contains a set of options to construct the internal webview with. + */ + struct Options { + /** + * If true, the user will be able to right click / inspect element / etc. If false, that behaviour is disabled. + */ + bool enableDebug{ false }; + /** + * If not serving from ram, leave this as a nullptr, and call `navigate` instead.
+ * If serving the content from ram, the webview will query the backend for files to load, with a call to this lambda.
+ * As mentioned in the docs for Resource, you should load these at construction, and keep a map `route:Resource` on hand. This lambda then, should query that map for the requested route, and return the associated resource if + * it exists, std::nullopt otherwise. Assuming a `std::unordered_map` called `m_resourceMap`, an implementation could be along the lines of: + * + * ```cpp + * const auto requested = url == "/" ? "/index.html" : url; + * const auto it = m_resources.find(requested); + * if (it == m_resources.end()) return {}; + * auto resource = it->second; + * return resource; + * ``` + * + */ + std::function(const std::string&)> contentProvider{ nullptr }; + /** + * An optional (javascript) script to be executed before the page loads. If you're hosting from RAM, prefer this to the internal webview's addInitScript function - this is because internally in choc, navigate is called before the init script + * is added, meaning that a script added with addInitScript won't execute until a refresh. This arg sidesteps that, by passing it into the internal webview's constructor. + */ + std::optional initScript{}; + }; /** * \param initialWidth The initial width for the webview. * \param initialHeight The initial height for the webview. + * \param backgroundColour The colour to paint the actual window beneath the webview. */ - WebviewEditor(std::uint32_t initialWidth, std::uint32_t initialHeight); + WebviewEditor(std::uint32_t initialWidth, std::uint32_t initialHeight, Colour backgroundColour); /** * Non default destructor for pimpl */ ~WebviewEditor() noexcept override; /** - * Specify some options to pass to the internal webview's constructor - note that this *must* be called *before* initialise (ie in your constructor) for them to get picked up. + * Specify some options to pass to the internal webview's constructor - note that this *must* be called *before* initialise (ie in your constructor) for them to get picked up.
* \param opts The options to pass to the internal webview */ - void setOptions(choc::ui::WebView::Options&& opts) noexcept; + void setOptions(Options&& options) noexcept; /** * Implementation of mostly_harmless::gui::IEditor::initialise(). * \param context The editor context (see IEditor::initialise() and EditorContext for more details). diff --git a/include/mostly_harmless/gui/platform/mostlyharmless_GuiHelpersMacOS.h b/include/mostly_harmless/gui/platform/mostlyharmless_GuiHelpersMacOS.h index cf7bc95..9d6da18 100644 --- a/include/mostly_harmless/gui/platform/mostlyharmless_GuiHelpersMacOS.h +++ b/include/mostly_harmless/gui/platform/mostlyharmless_GuiHelpersMacOS.h @@ -4,6 +4,7 @@ #ifndef MOSTLYHARMLESS_MOSTLYHARMLESS_GUIHELPERSMACOS_H #define MOSTLYHARMLESS_MOSTLYHARMLESS_GUIHELPERSMACOS_H +#include #include namespace mostly_harmless::gui::helpers::macos { /** @@ -30,7 +31,7 @@ namespace mostly_harmless::gui::helpers::macos { * \param parentViewHandle A void* to the NSView to add the child view to. * \param childViewHandle A void* to the NSView to add to the parent view. */ - void reparentView(void* parentViewHandle, void* childViewHandle, std::uint32_t backgroundColour); + void reparentView(void* parentViewHandle, void* childViewHandle, Colour backgroundColour); /** * Sets an NSView as visible. * \param viewHandle A void* to the NSView to set visible. diff --git a/modules/choc b/modules/choc index 42260d5..187e488 160000 --- a/modules/choc +++ b/modules/choc @@ -1 +1 @@ -Subproject commit 42260d583c1d4c14b34c1438409ed50cc67ee697 +Subproject commit 187e488de28f5cb3a889128bc7630b4448404915 diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt index 479a032..762bcc0 100644 --- a/source/CMakeLists.txt +++ b/source/CMakeLists.txt @@ -10,6 +10,7 @@ set(MOSTLYHARMLESS_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/mostlyharmless_Plugin.cpp ${CMAKE_CURRENT_SOURCE_DIR}/events/mostlyharmless_EventContext.cpp ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_WebviewEditor.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/gui/mostlyharmless_Colour.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_TaskThread.cpp ${CMAKE_CURRENT_SOURCE_DIR}/utils/mostlyharmless_Timer.cpp ${PLATFORM_SOURCES} diff --git a/source/gui/mostlyharmless_Colour.cpp b/source/gui/mostlyharmless_Colour.cpp new file mode 100644 index 0000000..500b430 --- /dev/null +++ b/source/gui/mostlyharmless_Colour.cpp @@ -0,0 +1,18 @@ +// +// Created by Syl on 08/09/2024. +// +#include +namespace mostly_harmless::gui { + Colour::Colour(std::uint32_t argb) { + a = static_cast((argb >> 24) & 0xFF); + r = static_cast((argb >> 16) & 0xFF); + g = static_cast((argb >> 8) & 0xFF); + b = static_cast(argb & 0xFF); + } + + Colour::Colour(std::uint8_t r_, std::uint8_t g_, std::uint8_t b_) : a(static_cast(255)), r(r_), g(g_), b(b_) { + } + + Colour::Colour(std::uint8_t a_, std::uint8_t r_, std::uint8_t g_, std::uint8_t b_) : a(a_), r(r_), g(g_), b(b_) { + } +} // namespace mostly_harmless::gui diff --git a/source/gui/mostlyharmless_WebviewEditor.cpp b/source/gui/mostlyharmless_WebviewEditor.cpp index 9d3bdcf..96e64f3 100644 --- a/source/gui/mostlyharmless_WebviewEditor.cpp +++ b/source/gui/mostlyharmless_WebviewEditor.cpp @@ -66,20 +66,44 @@ namespace mostly_harmless::gui { return s_mimeTypes[ext]; } + WebviewEditor::Resource::Resource(std::string_view content, std::string mimeType_) : mimeType(std::move(mimeType_)) { + if (!content.empty()) { + auto src = content.data(); + data.insert(data.end(), src, src + content.length()); + } + } + #if defined(MOSTLY_HARMLESS_WINDOWS) class WebviewEditor::Impl { public: - Impl(std::uint32_t initialWidth, std::uint32_t initialHeight) : m_initialWidth(initialWidth), m_initialHeight(initialHeight) { + Impl(std::uint32_t initialWidth, std::uint32_t initialHeight, Colour backgroundColour) : m_initialWidth(initialWidth), + m_initialHeight(initialHeight) { + m_brush = ::CreateSolidBrush(RGB(backgroundColour.r, backgroundColour.g, backgroundColour.b)); } - void setOptions(choc::ui::WebView::Options&& opts) { - m_options = std::move(opts); + ~Impl() noexcept { + ::DeleteObject(m_brush); + } + + void setOptions(Options&& options) { + m_options.enableDebugMode = options.enableDebug; + m_options.initScript = options.initScript; + if (options.contentProvider) { + auto wrapper = [options](const std::string& toFind) -> std::optional { + auto res = options.contentProvider(toFind); + if (!res) return {}; + choc::ui::WebView::Options::Resource resource; + resource.data = std::move(res->data); + resource.mimeType = std::move(res->mimeType); + return resource; + }; + m_options.fetchResource = std::move(wrapper); + }; } void create() { const auto iWidth = static_cast(m_initialWidth); const auto iHeight = static_cast(m_initialHeight); - ::SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "0"); m_webview = std::make_unique(m_options); auto* hwnd = static_cast<::HWND>(m_webview->getViewHandle()); ::SetWindowPos(hwnd, NULL, 0, 0, iWidth, iHeight, SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED); @@ -102,9 +126,12 @@ namespace mostly_harmless::gui { ::SetWindowPos(hwnd, NULL, 0, 0, static_cast(width), static_cast(height), SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOMOVE | SWP_FRAMECHANGED); } + void setParent(void* parentHandle) { auto* parentHwnd = static_cast(parentHandle); auto* handle = static_cast<::HWND>(m_webview->getViewHandle()); + ::SetClassLongPtrW(parentHwnd, GCLP_HBRBACKGROUND, (::LONG_PTR)m_brush); + ::InvalidateRect(parentHwnd, NULL, false); ::SetWindowLongPtrW(handle, GWL_STYLE, WS_CHILD); ::SetParent(handle, parentHwnd); show(); @@ -126,19 +153,36 @@ namespace mostly_harmless::gui { private: std::uint32_t m_initialWidth{ 0 }, m_initialHeight{ 0 }; - choc::ui::WebView::Options m_options; + ::HBRUSH m_brush{ nullptr }; + choc::ui::WebView::Options m_options{ + .enableDebugMode = false, + .transparentBackground = true, + }; std::unique_ptr m_webview{ nullptr }; }; #elif defined(MOSTLY_HARMLESS_MACOS) class WebviewEditor::Impl { public: - Impl(std::uint32_t initialWidth, std::uint32_t initialHeight) : m_initialWidth(initialWidth), - m_initialHeight(initialHeight) { + Impl(std::uint32_t initialWidth, std::uint32_t initialHeight, Colour backgroundColour) : m_initialWidth(initialWidth), + m_initialHeight(initialHeight), + m_backgroundColour(backgroundColour){ } - void setOptions(choc::ui::WebView::Options&& opts) { - m_options = std::move(opts); + void setOptions(Options&& options) { + m_options.enableDebugMode = options.enableDebug; + m_options.initScript = options.initScript; + if (options.contentProvider) { + auto wrapper = [options](const std::string& toFind) -> std::optional { + auto res = options.contentProvider(toFind); + if (!res) return {}; + choc::ui::WebView::Options::Resource resource; + resource.data = std::move(res->data); + resource.mimeType = std::move(res->mimeType); + return resource; + }; + m_options.fetchResource = std::move(wrapper); + }; } void create() { @@ -160,7 +204,7 @@ namespace mostly_harmless::gui { } void setParent(void* parentHandle) { - helpers::macos::reparentView(parentHandle, m_webview->getViewHandle(), m_options.backgroundColour); + helpers::macos::reparentView(parentHandle, m_webview->getViewHandle(), m_backgroundColour); } void show() { @@ -177,21 +221,26 @@ namespace mostly_harmless::gui { private: std::uint32_t m_initialWidth{ 0 }, m_initialHeight{ 0 }; - choc::ui::WebView::Options m_options; + choc::ui::WebView::Options m_options { + .enableDebugMode = false, + .transparentBackground = true + }; std::unique_ptr m_webview{ nullptr }; + Colour m_backgroundColour; }; #else static_assert(false); #endif - WebviewEditor::WebviewEditor(std::uint32_t initialWidth, std::uint32_t initialHeight) { - m_impl = std::make_unique(initialWidth, initialHeight); + WebviewEditor::WebviewEditor(std::uint32_t initialWidth, std::uint32_t initialHeight, Colour backgroundColour) { + m_impl = std::make_unique(initialWidth, initialHeight, backgroundColour); } WebviewEditor::~WebviewEditor() noexcept { } - void WebviewEditor::setOptions(choc::ui::WebView::Options&& opts) noexcept { - m_impl->setOptions(std::move(opts)); + void WebviewEditor::setOptions(Options&& options) noexcept { + + m_impl->setOptions(std::move(options)); } void WebviewEditor::initialise(EditorContext /*context*/) { diff --git a/source/gui/platform/mostlyharmless_GuiHelpersMacOS.mm b/source/gui/platform/mostlyharmless_GuiHelpersMacOS.mm index a2420ae..39caf95 100644 --- a/source/gui/platform/mostlyharmless_GuiHelpersMacOS.mm +++ b/source/gui/platform/mostlyharmless_GuiHelpersMacOS.mm @@ -20,13 +20,14 @@ void getViewSize(void* viewHandle, std::uint32_t* width, std::uint32_t* height) *height = static_cast(bounds.size.height); } - void reparentView(void* parentViewHandle, void* childViewHandle, std::uint32_t backgroundColour) { + void reparentView(void* parentViewHandle, void* childViewHandle, Colour backgroundColour) { auto* parent = static_cast(parentViewHandle); [parent setWantsLayer:true]; - const auto r = (backgroundColour >> 16) & 0xFF; - const auto g = (backgroundColour >> 8) & 0xFF; - const auto b = backgroundColour & 0xFF; - auto* color = [NSColor colorWithCalibratedRed:r green:g blue:b alpha:1]; + [[maybe_unused]] const auto [a, r, g, b] = backgroundColour; + CGFloat f32R = static_cast(r) / 255.0f; + CGFloat f32G = static_cast(g) / 255.0f; + CGFloat f32B = static_cast(b) / 255.0f; + auto* color = [NSColor colorWithCalibratedRed:f32R green:f32G blue:f32B alpha:1]; [[parent layer] setBackgroundColor:color.CGColor]; auto* child = static_cast(childViewHandle); child.frame = parent.bounds;