Tracktion / choc

A collection of header only classes, permissively licensed, to provide basic useful tasks with the bare-minimum of dependencies.
Other
543 stars 49 forks source link

custom ContextView for DesktopWindow #17

Closed mightgoyardstill closed 1 year ago

mightgoyardstill commented 1 year ago

been messing about with the DesktopWindow and really enjoy how easy it is to get it set up with a WebView. Thought I'd have a go at making a custom ContextView (currently its only for mac but i tried my best to follow the choc style)

//==================================================================================
namespace choc::ui {
class Context
{
public:
    //==============================================================================
    Context (Bounds b);
    Context (const Context&) = delete;
    Context (Context&&) = default;
    Context& operator= (Context&&) = default;
    ~Context() = default;
    //==============================================================================
    void* getViewHandle() const;
    //==============================================================================
    void repaint();
    //==============================================================================
    std::function<void(int x, int y)> mousePressed;
    std::function<void(int x, int y)> mouseReleased;
    std::function<void(int x, int y)> mouseMoved;
    std::function<void(int x, int y)> mouseDragged;
    //==============================================================================
protected:
    //==============================================================================
private:
    //==============================================================================
    struct Pimpl;
    std::unique_ptr<Pimpl> pimpl;
};
} // namespace choc::ui

//==================================================================================
//   Code beyond this point is implementation detail...
//==================================================================================

#if CHOC_APPLE
#include "gui/choc_MessageLoop.h"

struct choc::ui::Context::Pimpl
{
    //==============================================================================
    Pimpl (Bounds b, Context& c) : owner(c)
    {
        objc::AutoReleasePool autoreleasePool;
        view = createDelegate();

        objc::call<void>(view, "initWithFrame:", createCGRect(b));
        objc_setAssociatedObject (view, "choc_context", (id)this, OBJC_ASSOCIATION_ASSIGN);

        // Create a tracking area that covers the entire view
        id trackingArea = objc::call<id>(objc::call<id>(objc::getClass("NSTrackingArea"), "new"), 
                                        "initWithRect:options:owner:userInfo:", 
                                        createCGRect(b), 
                                        NSTrackingMouseMoved | NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect, 
                                        view, 
                                        nullptr);

        // Add the tracking area to the view
        objc::call<void>(view, "addTrackingArea:", trackingArea);
    }
    //==============================================================================
    ~Pimpl()        
    {
        objc::AutoReleasePool autoreleasePool;
        objc::call<void> (view, "release");
    }
    //==============================================================================
    void* getViewHandle() const { return (void*)view; }
    //==============================================================================
    void repaint()
    {
        objc::AutoReleasePool autoreleasePool;
        objc::call<void> (view, "drawRect:");
    }
    //==============================================================================
    static Pimpl& getPimplFromContext (id self)
    {
        auto view = (Pimpl*) objc_getAssociatedObject (self, "choc_context");
        CHOC_ASSERT (view != nullptr);
        return *view;
    }
    //==============================================================================
    id createDelegate()
    {
        static DelegateClass dc;
        return objc::call<id> ((id)dc.delegateClass, "new");
    }
    //==============================================================================
    Context& owner;
    id view = {};
    //==============================================================================
    struct DelegateClass
    {
        DelegateClass()
        {
            // Create a new NSView subclass and assign it to delegateClass
            delegateClass = objc::createDelegateClass ("NSView", "CHOCContextDelegate_");
            //======================================================================
            class_addMethod(delegateClass, sel_registerName("acceptsFirstResponder"),
            (IMP) (+[](id self, SEL) -> BOOL 
            {
                return YES; 
            }),
            "c@:");
            //======================================================================
            class_addMethod(delegateClass, sel_registerName("mouseDown:"),
            (IMP) (+[](id self, SEL, id event)
            {
                objc::AutoReleasePool autoreleasePool;
                CGPoint location = objc::call<CGPoint>(event, "locationInWindow");

                if (auto callback = getPimplFromContext (self).owner.mousePressed)
                    callback(location.x, location.y);
            }),
            "v@:@");
            //======================================================================
            class_addMethod(delegateClass, sel_registerName("mouseUp:"),
            (IMP) (+[](id self, SEL, id event)
            {
                objc::AutoReleasePool autoreleasePool;
                CGPoint location = objc::call<CGPoint>(event, "locationInWindow");

                if (auto callback = getPimplFromContext (self).owner.mouseReleased)
                    callback(location.x, location.y);
            }),
            "v@:@");
            //======================================================================
            class_addMethod(delegateClass, sel_registerName("mouseMoved:"),
            (IMP) (+[](id self, SEL, id event)
            {
                objc::AutoReleasePool autoreleasePool;
                CGPoint location = objc::call<CGPoint>(event, "locationInWindow");

                if (auto callback = getPimplFromContext (self).owner.mouseMoved)
                    callback(location.x, location.y);
            }),
            "v@:@");
            //======================================================================
            class_addMethod(delegateClass, sel_registerName("mouseDragged:"),
            (IMP) (+[](id self, SEL, id event)
            {
                objc::AutoReleasePool autoreleasePool;
                CGPoint location = objc::call<CGPoint>(event, "locationInWindow");

                if (auto callback = getPimplFromContext (self).owner.mouseDragged)
                    callback(location.x, location.y);
            }),
            "v@:@");
            //======================================================================
            class_addMethod(delegateClass, sel_registerName("drawRect:"),
            (IMP) (+[](id self, SEL, CGRect rect)
            {
                std::cout << "repaint()\n";
            }),
            "v@:{CGRect={CGPoint=dd}{CGSize=dd}}");
            //======================================================================
            objc_registerClassPair (delegateClass);
            //======================================================================
        }
        //==========================================================================
        ~DelegateClass() { objc_disposeClassPair (delegateClass); }
        //==========================================================================
        Class delegateClass = {};
    };
    //==============================================================================
    static constexpr long NSTrackingMouseMoved = 0x2;
    static constexpr long NSTrackingActiveInKeyWindow = 0x40;
    static constexpr long NSTrackingInVisibleRect = 0x200;
    //==============================================================================
};

#endif // CHOC_APPLE

namespace choc::ui
{
    inline Context::Context(Bounds b) : pimpl(std::make_unique<Pimpl>(b, *this)) {}
    inline void* Context::getViewHandle() const { return pimpl->getViewHandle(); }
    inline void Context::repaint() { pimpl->repaint(); }
}

#endif // CHOC_CONTEXT_HEADER_INCLUDED

i recognise this probably isn't the best place to open up a discussion so feel free to close it, but i was wondering if you were planning to do something along the lines of this? (i know the DesktopWindow is mainly built to host a webview but would be awesome to have a really basic non-webview type of context) my plan is to extend it a bit more so it can do some really basic drawing

mightgoyardstill commented 1 year ago

also the choc::objc stuff is awesome! makes life a lot easier

julianstorer commented 1 year ago

I'm not sure I really understand what you mean by a "context" here..? What's the use-case?

I'm certainly not keen to get caught up in creating a view that does things like e.g. allowing you to draw graphics and handle mouse events - that'd be rabbit-hole that could eat up years of time!

mightgoyardstill commented 1 year ago

yeah, unfortunately that is the exact use case! this would mainly be for some real basic drawing commands.. no real gui's like buttons, sliders etc - maybe something that could be a basic starting point for anyone to build their own themselves?

i've updated my initial comment with some new code that follows more along with how you structured the DesktopWindow and its looking a lot cleaner! (unfortunately i can only do it for the mac implementation) but it can now track basic mouse callbacks and doesn't interfere with the DesktopWindow callbacks (it also automatically calls drawRect on the NSView when the window is resized which is great!)

maybe context wasn't the right word, curious to know what you would name it? i'm quite happy to go down the rabbit hole for a little bit and see where it takes me, i didn't realise this was something that would take up years of time! any hints or tips on things you would consider if you were to do this yourself and why it would take so long?

heres how its looking in my main.cpp right now - just really love how i can see what everything is doing!

class Application {
public:
    void run() {
        choc::messageloop::run();
    }
    void stop() {
        choc::messageloop::stop();
    }
};

int main()
{
    Application application;

    choc::ui::Bounds b{500, 100, 300, 300};
    choc::ui::DesktopWindow window(b);
    choc::ui::Context context(b);

    window.setWindowTitle ("Hello World");
    window.setResizable (true);
    window.setMinimumSize (300, 300);
    window.setMaximumSize (1500, 1200);

    context.mousePressed = [](int x, int y) {
        std::cout << "pressed: " << x << ", " << y << "\n"; 
    };
    context.mouseReleased = [](int x, int y) {
        std::cout << "released: " << x << ", " << y << "\n";
    };
    context.mouseMoved = [](int x, int y) {
        std::cout << "moved: " << x << ", " << y << "\n";
    };
    context.mouseDragged = [](int x, int y) {
        std::cout << "dragged: " << x << ", " << y << "\n";
    };

    window.windowClosed = [&application]() {
        application.stop();
    };

    window.setContent(context.getViewHandle());
    window.toFront();
    application.run();
    return 0;
}

maybe another question worth asking is if you were going to use something other than a WebView to display in your window what would you typically go for? or was it primarily designed with WebView in mind?

julianstorer commented 1 year ago

I created the window class specifically because I needed something to hold a webview. Sure, I know that it could be used for other things too, but the amount of work you'd need to put in to create a decent, fast, cross-platform graphics rendering layer would be really huge - believe me, I've done one before! And even then it'd only offer a fraction of what a webview can do..

If you want to tinker with something like that for your own amusement, then have fun! But I'd be very reluctant to merge anything like that into choc because of the amount of work it'd take to review and maintain it.

mightgoyardstill commented 1 year ago

yeah thats fair! to be honest, i'm quite happy to mess about with it for my own amusement and wouldn't imagine it being merged into any of the existing choc stuff because of that anyway.

i guess i'm just more curious on if you have any pointers or tips for someone stupid enough to do their own graphics rendering layer.. or things you might've decided to do differently if you were to do it again lol.

ps. happy for you to close and park this as an issue now!

julianstorer commented 1 year ago

If you were to write a 2D renderer now, the best plan would be to have versions that use metal and vulkan, which would be a ton of work! Have fun if you go down that rabbithole!