Created
June 20, 2022 22:37
-
-
Save joeycastillo/ef2b449d40eaa14cc126c3248621cad4 to your computer and use it in GitHub Desktop.
A simple Focus-based app for tracking progress on projects
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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! | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment