Skip to content

Instantly share code, notes, and snippets.

@joeycastillo
Created June 20, 2022 22:37
Show Gist options
  • Select an option

  • Save joeycastillo/ef2b449d40eaa14cc126c3248621cad4 to your computer and use it in GitHub Desktop.

Select an option

Save joeycastillo/ef2b449d40eaa14cc126c3248621cad4 to your computer and use it in GitHub Desktop.

Revisions

  1. joeycastillo created this gist Jun 20, 2022.
    448 changes: 448 additions & 0 deletions Focus.cpp
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,448 @@
    #include "Focus.h"
    #include "Arduino.h"
    #include <algorithm>

    Task::Task() {
    }

    View::View(Rect rect) {
    this->frame = rect;
    this->window.reset();
    this->superview.reset();
    }

    View::~View() {
    }

    void View::draw(Adafruit_GFX *display, int16_t x, int16_t y) {
    if (this->opaque || this->backgroundColor) {
    display->fillRect(x + this->frame.origin.x, y + this->frame.origin.y, this->frame.size.width, this->frame.size.height, this->backgroundColor);
    }
    for(std::shared_ptr<View> view : this->subviews) {
    if (!view->hidden) view->draw(display, this->frame.origin.x, this->frame.origin.y);
    }
    }

    void View::addSubview(std::shared_ptr<View> view) {
    view->superview = this->shared_from_this();
    this->subviews.push_back(view);
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    view->setWindow(window);
    window->setNeedsDisplay(true);
    }
    }

    void View::removeSubview(std::shared_ptr<View> view) {
    if (view->isFocused()) {
    view->resignFocus();
    }
    view->superview.reset();
    view->window.reset();
    int index = std::distance(this->subviews.begin(), std::find(this->subviews.begin(), this->subviews.end(), view));
    this->subviews.erase(this->subviews.begin() + index);
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    // FIXME: We should only refocus if we know the focused view was removed.
    window->becomeFocused();
    window->setNeedsDisplay(true);
    }
    }

    bool View::isFocused() {
    return this->focused;
    }

    bool View::canBecomeFocused() {
    return false;
    }

    bool View::becomeFocused() {
    for(std::shared_ptr<View> subview : this->subviews) {
    if (subview->becomeFocused()) {
    return true;
    }
    }

    if (this->canBecomeFocused()) {
    // when there are no focusable subviews, and we can become
    // focused, become focused ourselves.
    // note that Window can always become focused, so this
    // block is guaranteed to execute when we reach the window.
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    std::shared_ptr<View> oldResponder = window->getFocusedView().lock();
    if (oldResponder != NULL) {
    oldResponder->willResignFocus();
    oldResponder->focused = false;
    window->focusedView.reset();
    oldResponder->didResignFocus();
    }
    this->willBecomeFocused();
    this->focused = true;
    window->focusedView = this->shared_from_this();
    this->didBecomeFocused();
    }

    return true;
    }

    return false;
    }

    void View::resignFocus() {
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    if (std::shared_ptr<View> superview = this->superview.lock()) {
    superview->becomeFocused();
    }
    }
    }

    void View::movedToWindow() {
    // nothing to do here
    }

    void View::willBecomeFocused() {
    // nothing to do here
    }

    void View::didBecomeFocused() {
    if (this->superview.lock()) {
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    std::shared_ptr<View> shared_this = this->shared_from_this();
    window->setNeedsDisplayInRect(this->frame, shared_this);
    }
    }
    }

    void View::willResignFocus() {
    // nothing to do here
    }

    void View::didResignFocus() {
    if (this->superview.lock()) {
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    std::shared_ptr<View> shared_this = this->shared_from_this();
    window->setNeedsDisplayInRect(this->frame, shared_this);
    }
    }
    }

    bool View::handleEvent(Event event) {
    std::shared_ptr<View> focusedView = NULL;
    std::shared_ptr<Window> window = NULL;
    if (window = this->getWindow().lock()) {
    focusedView = window->getFocusedView().lock();
    } else {
    focusedView = this->shared_from_this();
    if (focusedView == NULL) return false;
    window = std::static_pointer_cast<Window, View>(focusedView);
    }

    if (this->actions.count(event.type)) {
    if (std::shared_ptr<Application> application = window->application.lock()) {
    this->actions[event.type](event);
    }
    } else if (event.type < FOCUS_EVENT_BUTTON_TAP) {
    uint32_t index = std::distance(this->subviews.begin(), std::find(this->subviews.begin(), this->subviews.end(), focusedView));
    if (this->affinity == DirectionalAffinityVertical) {
    switch (event.type) {
    case FOCUS_EVENT_BUTTON_UP:
    while (index > 0) {
    if (this->subviews[index - 1]->canBecomeFocused()) this->subviews[index - 1]->becomeFocused();
    else index--;
    return true;
    }
    break;
    case FOCUS_EVENT_BUTTON_DOWN:
    while ((index + 1) < this->subviews.size()) {
    if (this->subviews[index + 1]->canBecomeFocused()) this->subviews[index + 1]->becomeFocused();
    else index--;
    return true;
    }
    break;
    default:
    break;
    }
    } else if (this->affinity == DirectionalAffinityHorizontal) {
    switch (event.type) {
    case FOCUS_EVENT_BUTTON_LEFT:
    while (index > 0) {
    if (this->subviews[index - 1]->canBecomeFocused()) this->subviews[index - 1]->becomeFocused();
    return true;
    }
    break;
    case FOCUS_EVENT_BUTTON_RIGHT:
    while ((index + 1) < this->subviews.size()) {
    if (this->subviews[index + 1]->canBecomeFocused()) this->subviews[index + 1]->becomeFocused();
    return true;
    }
    break;
    default:
    break;
    }
    }
    }
    if (std::shared_ptr<View> superview = this->superview.lock()) {
    superview->handleEvent(event);
    }

    return false;
    }

    void View::setAction(const Action &action, int32_t type) {
    this->actions[type] = action;
    }

    void View::removeAction(int32_t type) {
    // TODO: remove the action
    }

    std::weak_ptr<View> View::getSuperview() {
    return this->superview;
    }

    std::weak_ptr<Window> View::getWindow() {
    return this->window;
    }

    void View::setWindow(std::shared_ptr<Window>window) {
    this->window = window;
    for(std::shared_ptr<View> subview : this->subviews) {
    subview->setWindow(window);
    }
    }

    Rect View::getFrame() {
    return this->frame;
    }

    void View::setFrame(Rect frame) {
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    Rect dirtyRect = MakeRect(min(this->frame.origin.x, frame.origin.x), min(this->frame.origin.y, frame.origin.y), 0, 0);
    dirtyRect.size.width = max(this->frame.origin.x + this->frame.size.width, frame.origin.x + frame.size.width) - dirtyRect.origin.x;
    dirtyRect.size.height = max(this->frame.origin.y + this->frame.size.height, frame.origin.y + frame.size.height) - dirtyRect.origin.y;
    this->frame = frame;
    window->setNeedsDisplayInRect(dirtyRect, window);
    }
    }

    bool View::isOpaque() {
    return this->opaque;
    }

    void View::setOpaque(bool value) {
    if (this-> opaque == value) return;

    this->opaque = value;
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    window->setNeedsDisplayInRect(this->frame, window);
    }
    }

    bool View::isHidden() {
    return this->hidden;
    }

    void View::setHidden(bool value) {
    if (this-> hidden == value) return;

    this->hidden = value;
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    window->setNeedsDisplayInRect(this->frame, window);
    }
    }

    int32_t View::getTag() {
    return this->tag;
    }

    void View::setTag(int32_t value) {
    this->tag = value;
    }

    uint16_t View::getBackgroundColor() {
    return this->backgroundColor;
    }

    void View::setBackgroundColor(uint16_t value) {
    this->backgroundColor = value;
    }

    uint16_t View::getForegroundColor() {
    return this->foregroundColor;
    }

    void View::setForegroundColor(uint16_t value) {
    this->foregroundColor = value;
    }

    uint16_t View::getDirectionalAffinity() {
    return this->affinity;
    }

    void View::setDirectionalAffinity(DirectionalAffinity value) {
    this->affinity = value;
    }

    Control::Control(Rect rect) : View(rect) {
    }

    bool Control::isEnabled() {
    return this->enabled;
    }

    void Control::setEnabled(bool value) {
    this->enabled = value;
    }

    bool Control::canBecomeFocused() {
    return this->enabled;
    }

    Window::Window(Size size) : View(MakeRect(0, 0, size.width, size.height)) {
    this->setNeedsDisplay(true);
    }

    void Window::addSubview(std::shared_ptr<View> view) {
    view->setWindow(std::static_pointer_cast<Window>(this->shared_from_this()));
    View::addSubview(view);
    // when we add a new view hierarchy to the window, try to focus on its innermost view.
    this->becomeFocused();
    }

    bool Window::canBecomeFocused() {
    return true;
    }

    bool Window::needsDisplay() {
    return this->dirty;
    }

    void Window::setNeedsDisplay(bool needsDisplay) {
    if (needsDisplay) {
    this->dirtyRect = MakeRect(0, 0, this->frame.size.width, this->frame.size.height);
    this->dirty = true;
    } else {
    this->dirty = false;
    }
    }

    void Window::setNeedsDisplayInRect(Rect rect, std::shared_ptr<View> view) {
    std::shared_ptr<View> superview(view);
    while(superview = superview->superview.lock()) {
    rect.origin.x += superview->frame.origin.x;
    rect.origin.y += superview->frame.origin.y;
    }

    Rect finalRect;
    if (this->dirty) {
    finalRect = MakeRect(min(this->dirtyRect.origin.x, rect.origin.x), min(this->dirtyRect.origin.y, rect.origin.y), 0, 0);
    finalRect.size.width = max(this->dirtyRect.origin.x + this->dirtyRect.size.width, rect.origin.x + rect.size.width) - finalRect.origin.x;
    finalRect.size.height = max(this->dirtyRect.origin.y + this->dirtyRect.size.height, rect.origin.y + rect.size.height) - finalRect.origin.y;
    } else {
    finalRect = rect;
    }

    this->dirty = true;
    this->dirtyRect = finalRect;
    }

    Rect Window::getDirtyRect() {
    if (this->dirty) return this->dirtyRect;
    else return {0};
    }

    std::weak_ptr<View> Window::getFocusedView() {
    return this->focusedView;
    }

    std::weak_ptr<View> Window::getSuperview() {
    return std::weak_ptr<View>();
    }

    std::weak_ptr<Window> Window::getWindow() {
    return std::static_pointer_cast<Window, View>(this->shared_from_this());
    }

    void Window::setWindow(std::shared_ptr<Window> window) {
    // nothing to do here
    }

    Application::Application(const std::shared_ptr<Window>& window) {
    this->window = window;
    }

    void Application::addTask(std::shared_ptr<Task> task) {
    this->tasks.push_back(task);
    }

    void Application::run() {
    this->window->application = this->shared_from_this();
    this->window->becomeFocused();
    this->window->setNeedsDisplay(true);
    while(true) {
    for(std::shared_ptr<Task> task : this->tasks) {
    if (task->run(this) != 0) return;
    }
    }
    }

    void Application::generateEvent(int32_t eventType, int32_t userInfo) {
    Event event;
    event.type = eventType;
    event.userInfo = userInfo;
    if (std::shared_ptr<View> focusedView = this->window->focusedView.lock()) {
    focusedView->handleEvent(event);
    }
    }

    std::shared_ptr<Window> Application::getWindow() {
    return this->window;
    }

    void Application::setRootViewController(std::shared_ptr<ViewController> viewController) {
    if (this->rootViewController) {
    // clean up old view controller
    this->rootViewController->viewWillDisappear();
    this->window->removeSubview(this->rootViewController->view);
    this->rootViewController->viewDidDisappear();
    }

    // set up new view controller
    this->rootViewController = viewController;
    this->rootViewController->viewWillAppear();
    this->window->addSubview(this->rootViewController->view);
    this->rootViewController->viewDidAppear();
    }

    void ViewController::viewWillAppear() {
    if (!this->view) {
    this->createView();
    }
    }

    void ViewController::viewDidDisappear() {
    this->destroyView();
    }

    void ViewController::generateEvent(int32_t eventType, int32_t userInfo) {
    if (!this->view) return;

    // unsure about this one: we generate an event and let it bubble up to the window,
    // where the application can listen for it. seems like wasted effort to get a message
    // from a view controller to the application.
    if (std::shared_ptr<Window> window = this->view->getWindow().lock()) {
    if (std::shared_ptr<Application> application = window->application.lock()) {
    application->generateEvent(eventType, userInfo);
    }
    }
    }

    void ViewController::createView() {
    if (this->view) {
    this->destroyView();
    }
    // subclasses must override to create view here
    }

    void ViewController::destroyView() {
    this->view.reset();
    }
    190 changes: 190 additions & 0 deletions Focus.h
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,190 @@
    #ifndef Focus_h
    #define Focus_h

    #include <stdint.h>
    #include <vector>
    #include <memory>
    #include <map>
    #include <functional>
    #include "Adafruit_GFX.h"

    #define FOCUS_EVENT_BUTTON_LEFT (0)
    #define FOCUS_EVENT_BUTTON_DOWN (1)
    #define FOCUS_EVENT_BUTTON_UP (2)
    #define FOCUS_EVENT_BUTTON_RIGHT (3)
    #define FOCUS_EVENT_BUTTON_TAP (4)
    #define FOCUS_EVENT_BUTTON_PREV (5)
    #define FOCUS_EVENT_BUTTON_NEXT (6)
    #define FOCUS_EVENT_BUTTON_LOCK (7)

    typedef struct {
    int16_t x;
    int16_t y;
    } Point;

    typedef struct {
    int16_t width;
    int16_t height;
    } Size;

    typedef struct {
    Point origin;
    Size size;
    } Rect;

    inline Point MakePoint(int16_t x, int16_t y) { return {x, y}; }
    inline Size MakeSize(int16_t width, int16_t height) { return {width, height}; }
    inline Rect MakeRect(int16_t x, int16_t y, int16_t width, int16_t height) { return {{x, y}, {width, height}}; }

    inline bool PointsEqual(Point a, Point b) { return (a.x == b.x) && (a.y == b.y); }
    inline bool SizesEqual(Size a, Size b) { return (a.width == b.width) && (a.height == b.height); }
    inline bool RectsEqual(Rect a, Rect b) { return PointsEqual(a.origin, b.origin) && SizesEqual(a.size, b.size); }

    class Application;
    class Window;
    class View;
    class Task;
    class ViewController;

    typedef struct {
    int32_t type;
    int32_t userInfo;
    } Event;

    typedef enum {
    DirectionalAffinityVertical,
    DirectionalAffinityHorizontal,
    } DirectionalAffinity;

    typedef std::function<void(Event)> Action;

    class Task {
    public:
    Task();
    virtual int16_t run(Application *application) = 0;
    };

    class View : public std::enable_shared_from_this<View> {
    public:
    View(Rect rect);
    ~View();
    virtual void draw(Adafruit_GFX *display, int16_t x, int16_t y);
    virtual void addSubview(std::shared_ptr<View> view);
    void removeSubview(std::shared_ptr<View> view);
    bool isFocused();
    virtual bool canBecomeFocused();
    virtual bool becomeFocused();
    virtual void resignFocus();
    virtual void movedToWindow();
    virtual void willBecomeFocused();
    virtual void didBecomeFocused();
    virtual void willResignFocus();
    virtual void didResignFocus();
    virtual bool handleEvent(Event event);
    void setAction(const Action &action, int32_t type);
    void removeAction(int32_t type);
    virtual std::weak_ptr<View>getSuperview();
    virtual std::weak_ptr<Window> getWindow();
    virtual void setWindow(std::shared_ptr<Window> window);
    Rect getFrame();
    void setFrame(Rect rect);
    bool isOpaque();
    void setOpaque(bool value);
    bool isHidden();
    void setHidden(bool value);
    int32_t getTag();
    void setTag(int32_t value);
    uint16_t getBackgroundColor();
    void setBackgroundColor(uint16_t value);
    uint16_t getForegroundColor();
    void setForegroundColor(uint16_t value);
    uint16_t getDirectionalAffinity();
    void setDirectionalAffinity(DirectionalAffinity value);
    protected:
    bool focused = false;
    bool opaque = false;
    bool hidden = false;
    int32_t tag = 0;
    uint16_t backgroundColor = 0;
    uint16_t foregroundColor = 1;
    Rect frame = {0};
    DirectionalAffinity affinity = DirectionalAffinityVertical;
    std::vector<std::shared_ptr<View>> subviews;
    std::map<int32_t, Action> actions;
    std::weak_ptr<View> superview;
    private:
    std::weak_ptr<Window> window;

    friend class Window;
    };

    class Control : public View {
    public:
    Control(Rect rect);
    bool isEnabled();
    void setEnabled(bool value);
    bool canBecomeFocused() override;
    protected:
    bool enabled = true;
    };

    class Window : public View {
    public:
    Window(Size size);
    void addSubview(std::shared_ptr<View> view) override;
    bool canBecomeFocused() override;
    bool needsDisplay();
    void setNeedsDisplay(bool needsDisplay);
    void setNeedsDisplayInRect(Rect rect, std::shared_ptr<View> view);
    Rect getDirtyRect();
    std::weak_ptr<View> getFocusedView();
    std::weak_ptr<View>getSuperview() override;
    std::weak_ptr<Window> getWindow() override;
    void setWindow(std::shared_ptr<Window> window) override;
    protected:
    std::weak_ptr<Application> application;
    std::weak_ptr<View> focusedView;
    bool dirty;
    Rect dirtyRect;

    friend class Application;
    friend class View;
    friend class ViewController;
    };

    class Application : public std::enable_shared_from_this<Application> {
    public:
    Application(const std::shared_ptr<Window>& window);
    void run();
    void addTask(std::shared_ptr<Task> task);
    void generateEvent(int32_t eventType, int32_t userInfo);
    std::shared_ptr<Window> getWindow();

    void setRootViewController(std::shared_ptr<ViewController> viewController);

    protected:
    std::vector<std::shared_ptr<Task>> tasks;
    std::shared_ptr<Window> window;
    std::shared_ptr<ViewController> rootViewController;
    };

    class ViewController : public std::enable_shared_from_this<ViewController> {
    public:
    ViewController() {};

    virtual void viewWillAppear();
    virtual void viewDidAppear() {};
    virtual void viewWillDisappear() {};
    virtual void viewDidDisappear();

    void generateEvent(int32_t eventType, int32_t userInfo = 0);

    protected:
    virtual void createView();
    virtual void destroyView();
    std::shared_ptr<View> view;

    friend class Application;
    };

    #endif // Focus_h
    86 changes: 86 additions & 0 deletions FocusWidgets.cpp
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,86 @@
    #include "FocusWidgets.h"
    #include <algorithm>

    BitmapView::BitmapView(Rect rect, const unsigned char *bitmap) : View(rect) {
    this->bitmap = bitmap;
    }

    void BitmapView::draw(Adafruit_GFX *display, int16_t x, int16_t y) {
    View::draw(display, x, y);
    display->drawBitmap(this->frame.origin.x, this->frame.origin.y, this->bitmap, this->frame.size.width, this->frame.size.height, this->foregroundColor);
    }

    Button::Button(Rect rect, std::string text) : Control(rect) {
    this->text = text;
    }

    void Button::draw(Adafruit_GFX *display, int16_t x, int16_t y) {
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    View::draw(display, x, y);
    display->setCursor(this->frame.origin.x + x + 4, this->frame.origin.y + y + this->frame.size.height / 2 - 4);
    if (this->focused) {
    display->fillRect(x + this->frame.origin.x, y + this->frame.origin.y, this->frame.size.width, this->frame.size.height, this->foregroundColor);
    display->setTextColor(this->backgroundColor);
    display->print(this->text.c_str());
    } else {
    display->drawRect(x + this->frame.origin.x, y + this->frame.origin.y, this->frame.size.width, this->frame.size.height, this->foregroundColor);
    display->setTextColor(this->foregroundColor);
    display->print(this->text.c_str());
    }
    }
    }

    HatchedView::HatchedView(Rect rect, uint16_t color) : View(rect) {
    this->foregroundColor = color;
    }

    void HatchedView::draw(Adafruit_GFX *display, int16_t x, int16_t y) {
    for(int16_t i = x; i < x + this->frame.size.width; i++) {
    for(int16_t j = y; j < y + this->frame.size.height; j++) {
    if ((i + j) % 2) {
    display->drawPixel(i, j, this->foregroundColor);
    }
    }
    }
    View::draw(display, x, y);
    }

    BorderedView::BorderedView(Rect rect) : View(rect) {
    this->opaque = true;
    }

    void BorderedView::draw(Adafruit_GFX *display, int16_t x, int16_t y) {
    View::draw(display, x, y);
    display->drawRect(x + this->frame.origin.x, y + this->frame.origin.y, this->frame.size.width, this->frame.size.height, this->foregroundColor);
    }

    void ProgressView::draw(Adafruit_GFX *display, int16_t x, int16_t y) {
    View::draw(display, x, y);
    display->fillRect(x + this->frame.origin.x, y + this->frame.origin.y, this->frame.size.width, this->frame.size.height, this->backgroundColor);
    display->fillRect(x + this->frame.origin.x, y + this->frame.origin.y, (int16_t)(this->frame.size.width * this->progress), this->frame.size.height, this->foregroundColor);
    }

    void ProgressView::setProgress(float value) {
    this->progress = value;
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    window->setNeedsDisplayInRect(this->frame, window);
    }
    }

    Label::Label(Rect rect, std::string text) : View(rect) {
    this->text = text;
    }

    void Label::draw(Adafruit_GFX *display, int16_t x, int16_t y) {
    View::draw(display, x, y);
    display->setTextColor(this->foregroundColor);
    display->setCursor(this->frame.origin.x + x, this->frame.origin.y + y);
    display->print(this->text.c_str());
    }

    void Label::setText(std::string text) {
    this->text = text;
    if (std::shared_ptr<Window> window = this->getWindow().lock()) {
    window->setNeedsDisplayInRect(this->frame, window);
    }
    }
    56 changes: 56 additions & 0 deletions FocusWidgets.h
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,56 @@
    #ifndef FocusWidgets_h
    #define FocusWidgets_h

    #include <stdint.h>
    #include <vector>
    #include <map>
    #include <string>
    #include "Focus.h"

    class BitmapView : public View {
    public:
    BitmapView(Rect rect, const unsigned char *bitmap);
    void draw(Adafruit_GFX *display, int16_t x, int16_t y) override;
    protected:
    const unsigned char *bitmap;
    };

    class Button : public Control {
    public:
    Button(Rect rect, std::string text);
    void draw(Adafruit_GFX *display, int16_t x, int16_t y) override;
    protected:
    std::string text;
    };

    class HatchedView : public View {
    public:
    HatchedView(Rect rect, uint16_t color);
    void draw(Adafruit_GFX *display, int16_t x, int16_t y) override;
    };

    class BorderedView : public View {
    public:
    BorderedView(Rect rect);
    void draw(Adafruit_GFX *display, int16_t x, int16_t y) override;
    };

    class ProgressView : public View {
    public:
    ProgressView(Rect rect) : View(rect) {};
    void draw(Adafruit_GFX *display, int16_t x, int16_t y) override;
    void setProgress(float value);
    protected:
    float progress = 0;
    };

    class Label : public View {
    public:
    Label(Rect rect, std::string text);
    void draw(Adafruit_GFX *display, int16_t x, int16_t y) override;
    void setText(std::string text);
    protected:
    std::string text;
    };

    #endif // FocusWidgets_h
    230 changes: 230 additions & 0 deletions MagTag_Focus_ProjectTracker.ino
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,230 @@
    #include "Adafruit_ThinkInk.h"
    #include "Adafruit_NeoPixel.h"
    #include "Focus.h"
    #include "FocusWidgets.h"

    typedef struct {
    std::string title;
    uint8_t column;
    } Project;

    #define USER_EVENT_SHOW_HELP (10000)
    #define USER_EVENT_SHOW_PROJECTS (10001)

    class MyProjectsViewController : public ViewController {
    public:
    MyProjectsViewController(std::vector<Project> projects) {
    this->projects = projects;
    }
    protected:
    std::vector<Project> projects;
    std::vector<std::shared_ptr<Button>> cards;

    virtual void createView() override {
    ViewController::createView();
    this->view = std::make_shared<View>(MakeRect(0, 0, 296, 128));
    std::shared_ptr<Label> header = std::make_shared<Label>(MakeRect(0, 2, 296, 8), " Stagnant This Month This Week Up Next");
    this->view->addSubview(header);
    std::shared_ptr<BorderedView> divider = std::make_shared<BorderedView>(MakeRect(0, 12, 296, 2));
    this->view->addSubview(divider);
    std::shared_ptr<Button> helpButton = std::make_shared<Button>(MakeRect(282, 0, 14, 13), "?");
    helpButton->setBackgroundColor(EPD_LIGHT);
    this->view->addSubview(helpButton);
    helpButton->setAction(std::bind(&MyProjectsViewController::helpButtonPressed, this, std::placeholders::_1), FOCUS_EVENT_BUTTON_TAP);
    int16_t i = 0;
    for(Project project : this->projects) {
    std::shared_ptr<Button> card = std::make_shared<Button>(MakeRect(74 * project.column, 18 + (16 * i), 72, 14), project.title);
    card->setBackgroundColor(EPD_LIGHT);
    card->setTag(i++);
    card->setAction(std::bind(&MyProjectsViewController::moveCard, this, std::placeholders::_1), FOCUS_EVENT_BUTTON_LEFT);
    card->setAction(std::bind(&MyProjectsViewController::moveCard, this, std::placeholders::_1), FOCUS_EVENT_BUTTON_RIGHT);
    this->cards.push_back(card);
    this->view->addSubview(card);
    }
    this->_updateCardPositions();
    }

    virtual void destroyView() override {
    this->cards.clear();
    ViewController::destroyView();
    }

    void moveCard(Event event) {
    int selectedProjectIndex = -1;
    for(std::shared_ptr<Button> card : this->cards) {
    if (card->isFocused()) selectedProjectIndex = card->getTag();
    }

    // if no focused view found, shrug. don't do anything tho.
    if (selectedProjectIndex == -1) return;

    switch (event.type) {
    case FOCUS_EVENT_BUTTON_RIGHT:
    if (this->projects[selectedProjectIndex].column < 3) {
    this->projects[selectedProjectIndex].column++;
    }
    break;
    case FOCUS_EVENT_BUTTON_LEFT:
    if (this->projects[selectedProjectIndex].column > 0) {
    this->projects[selectedProjectIndex].column--;
    }
    break;
    default:
    return;
    }
    this->_updateCardPositions();
    }

    void helpButtonPressed(Event event) {
    this->generateEvent(USER_EVENT_SHOW_HELP);
    }

    protected:
    void _updateCardPositions() {
    for(int i = 0; i < this->projects.size(); i++) {
    Project project = this->projects[i];
    std::shared_ptr<Button> card= this->cards[i];
    card->setFrame(MakeRect(74 * project.column, 18 + (16 * i), 72, 14));
    }
    }
    };

    class HelpViewController : public ViewController {
    protected:
    virtual void createView() override {
    this->view = std::make_shared<View>(MakeRect(0, 0, 296, 128));
    this->view->setDirectionalAffinity(DirectionalAffinityHorizontal);
    std::shared_ptr<Label> label = std::make_shared<Label>(MakeRect(0, 18, 260, 60), " This project tracker lets you arrange your\n projects by priority and recency. Use the\n up and down buttons to select a project,\n and the left and right buttons to move\n it between the available columns.");
    this->view->addSubview(label);
    std::shared_ptr<Button> button1 = std::make_shared<Button>(MakeRect(10, 96, 108, 18), "OK");
    button1->setBackgroundColor(EPD_LIGHT);
    button1->setAction(std::bind(&HelpViewController::dismiss, this, std::placeholders::_1), FOCUS_EVENT_BUTTON_TAP);
    this->view->addSubview(button1);
    std::shared_ptr<Button> button2 = std::make_shared<Button>(MakeRect(128, 96, 160, 18), "OK, but with rainbows");
    button2->setBackgroundColor(EPD_LIGHT);
    button2->setAction(std::bind(&HelpViewController::dismissWithRainbows, this, std::placeholders::_1), FOCUS_EVENT_BUTTON_TAP);
    this->view->addSubview(button2);
    }

    void dismiss(Event event) {
    this->generateEvent(USER_EVENT_SHOW_PROJECTS);
    }

    void dismissWithRainbows(Event event) {
    pinMode(NEOPIXEL_POWER, OUTPUT);
    digitalWrite(NEOPIXEL_POWER, LOW);
    Adafruit_NeoPixel pixels = Adafruit_NeoPixel(4, PIN_NEOPIXEL, NEO_GRB + NEO_KHZ800);
    pixels.begin();
    for(int i = 0; i < 5; i++) {
    pixels.setPixelColor(0, pixels.Color(255, 0, 0));
    pixels.setPixelColor(1, pixels.Color(255, 255, 0));
    pixels.setPixelColor(2, pixels.Color(0, 255, 0));
    pixels.setPixelColor(3, pixels.Color(0, 0, 255));
    pixels.show();
    delay(250);
    pixels.fill(pixels.Color(0, 0, 0));
    pixels.show();
    delay(50);
    }
    digitalWrite(NEOPIXEL_POWER, HIGH);
    this->generateEvent(USER_EVENT_SHOW_PROJECTS);
    }
    };

    class ButtonInputTask : public Task {
    public:
    ButtonInputTask() {
    digitalWrite(LED_BUILTIN, LOW);
    pinMode(BUTTON_A, INPUT_PULLUP);
    pinMode(BUTTON_B, INPUT_PULLUP);
    pinMode(BUTTON_C, INPUT_PULLUP);
    pinMode(BUTTON_D, INPUT_PULLUP);
    pinMode(0, INPUT_PULLUP);
    };
    int16_t run(Application *application) {
    if (!digitalRead(BUTTON_A)) application->generateEvent(FOCUS_EVENT_BUTTON_LEFT, 0);
    if (!digitalRead(BUTTON_B)) application->generateEvent(FOCUS_EVENT_BUTTON_UP, 0);
    if (!digitalRead(BUTTON_C)) application->generateEvent(FOCUS_EVENT_BUTTON_DOWN, 0);
    if (!digitalRead(BUTTON_D)) application->generateEvent(FOCUS_EVENT_BUTTON_RIGHT, 0);
    if (!digitalRead(0)) application->generateEvent(FOCUS_EVENT_BUTTON_TAP, 0);

    return 0;
    }
    };

    class ThinkInkDisplayTask : public Task {
    public:
    ThinkInkDisplayTask() {
    this->display = new ThinkInk_290_Grayscale4_T5(EPD_DC, EPD_RESET, EPD_CS, -1, EPD_BUSY);
    this->display->begin(THINKINK_MONO);
    };
    int16_t run(Application *application) {
    std::shared_ptr<Window> window = application->getWindow();
    if (window->needsDisplay()) {
    this->display->clearBuffer();
    window->draw(this->display, 0, 0);
    Rect dirtyRect = window->getDirtyRect();
    if (RectsEqual(dirtyRect, window->getFrame())) {
    this->display->display();
    } else {
    display->displayPartial(dirtyRect.origin.x, dirtyRect.origin.y, dirtyRect.origin.x + dirtyRect.size.width, dirtyRect.origin.y + dirtyRect.size.height);
    }
    window->setNeedsDisplay(false);
    }

    return 0;
    }
    protected:
    ThinkInk_290_Grayscale4_T5 *display;
    };

    class ProjectTrackerApplication : public Application {
    public:
    ProjectTrackerApplication(const std::shared_ptr<Window>& window) : Application(window) {
    // Add an input task to generate events from button presses
    std::shared_ptr<Task> inputTask = std::make_shared<ButtonInputTask>();
    this->addTask(inputTask);
    // Add a display task to update the window
    std::shared_ptr<Task> displayTask = std::make_shared<ThinkInkDisplayTask>();
    this->addTask(displayTask);

    // Finally, set up our view controller with the tasks we want to display!
    std::vector<Project> projects;
    projects.push_back({"Project 1", 1});
    projects.push_back({"Project 2", 2});
    projects.push_back({"Project 3", 1});
    projects.push_back({"Project 4", 3});
    projects.push_back({"Project 5", 0});
    projects.push_back({"Project 6", 0});
    this->mainViewController = std::make_shared<MyProjectsViewController>(projects);
    this->setRootViewController(this->mainViewController);

    // The application can listen for Focus events as well as custom events, like this request to show the Help modal.
    this->window->setAction(std::bind(&ProjectTrackerApplication::showHelp, this, std::placeholders::_1), USER_EVENT_SHOW_HELP);
    this->window->setAction(std::bind(&ProjectTrackerApplication::returnHome, this, std::placeholders::_1), USER_EVENT_SHOW_PROJECTS);
    }

    void showHelp(Event event) {
    std::shared_ptr<ViewController> helpViewController = std::make_shared<HelpViewController>();
    this->setRootViewController(helpViewController);
    this->window->setNeedsDisplay(true);
    }

    void returnHome(Event event) {
    this->setRootViewController(this->mainViewController);
    this->window->setNeedsDisplay(true);
    }
    protected:
    std::shared_ptr<ViewController> mainViewController;
    };

    void setup() {
    std::shared_ptr<Window> window = std::make_shared<Window>(MakeSize(128, 296));
    std::shared_ptr<Application> application = std::make_shared<ProjectTrackerApplication>(window);

    application->run();
    }

    void loop() {
    // Nothing to do here!
    }