commit d51d778f920de90466bcffeef6b1a991e156ae87 Author: Jason von Nieda Date: Fri Jan 20 17:14:26 2012 -0800 Initial import diff --git a/ScreenUi.cpp b/ScreenUi.cpp new file mode 100644 index 0000000..fdc9463 --- /dev/null +++ b/ScreenUi.cpp @@ -0,0 +1,532 @@ +/** + * A toolkit for building character based user interfaces on small displays. + * + * Copyright (c) 2012 Jason von Nieda + * This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 + * Unported License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative + * Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. + */ + +#include +#include +#include +#include + +#ifdef SCREENUI_DEBUG +#include +#endif + +// TODO: Change to PROGMEM +uint8_t charCheckmark[] = {0, // B00000 + 0, // B00000 + 1, // B00001 + 2, // B00010 + 20, // B10100 + 8, // B01000 + 0, // B00000 + 0}; // B00000 + + +//////////////////////////////////////////////////////////////////////////////// +// Screen +//////////////////////////////////////////////////////////////////////////////// +Screen::Screen(uint8_t width, uint8_t height) { + setSize(width, height); + cleared_ = false; + focusHolder_ = NULL; + focusHolderSelected_ = false; + createCustomChar(7, charCheckmark); + annoyingBugWorkedAround_ = false; +} + +void Screen::update() { + if (!cleared_) { + clear(); + cleared_ = true; + } + Container::update(this); + int x, y; + bool selected, cancelled; + getInputDeltas(&x, &y, &selected, &cancelled); + Component *oldFocusHolder = focusHolder_; + if (x || y || selected || cancelled) { + if (focusHolderSelected_) { + focusHolderSelected_ = focusHolder_->handleInputEvent(x, y, selected, cancelled); + } + else { + if (selected) { + focusHolderSelected_ = focusHolder_->handleInputEvent(x, y, selected, cancelled); + } + else if (x || y) { + // TODO: Make axis x or y configurable. + if (y > 0) { + focusHolder_ = nextFocusHolder(focusHolder_, false); + if (!focusHolder_) { + focusHolder_ = nextFocusHolder(focusHolder_, false); + } + } + else if (y < 0) { + focusHolder_ = nextFocusHolder(focusHolder_, true); + if (!focusHolder_) { + focusHolder_ = nextFocusHolder(focusHolder_, true); + } + } + } + } + } + if (focusHolder_ == NULL) { + focusHolder_ = nextFocusHolder(focusHolder_, false); + } + if (oldFocusHolder != focusHolder_) { + if (oldFocusHolder) { + oldFocusHolder->repaint(); + } + focusHolder_->repaint(); + } + paint(this); + moveCursor(cursorX_, cursorY_); + + // TODO: Bug I can't figure out. If we use the dirtyness system, the first paint + // fails to draw anything to the screen and then subsequent ones don't get called + // because they aren't dirty. The second paint works fine. + // Adding repaint() here allows us to paint during the second loop which gets + // everything into a state where it works great, but I can't figure out why. + // It doesn't seem to have to do with timing or the clear(). + if (!annoyingBugWorkedAround_) { + repaint(); + annoyingBugWorkedAround_ = true; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Container +//////////////////////////////////////////////////////////////////////////////// +Container::Container() { + components_ = NULL; + componentsLength_ = 0; + componentCount_ = 0; +} + +Container::~Container() { + free(components_); +} + +void Container::update(Screen *screen) { + for (int i = 0; i < componentCount_; i++) { + components_[i]->update(screen); + } +} + +void Container::paint(Screen *screen) { + for (int i = 0; i < componentCount_; i++) { + if (components_[i]->dirty()) { + components_[i]->paint(screen); + } + } +} + +void Container::repaint() { + for (int i = 0; i < componentCount_; i++) { + components_[i]->repaint(); + } +} + +void Container::add(Component *component, int8_t x, int8_t y) { + if (!components_ || componentsLength_ <= componentCount_) { + uint8_t newComponentsLength = (componentsLength_ * 2) + 1; + Component** newComponents = (Component**) malloc(newComponentsLength * sizeof(Component*)); + for (int i = 0; i < componentCount_; i++) { + newComponents[i] = components_[i]; + } + free(components_); + components_ = newComponents; + componentsLength_ = newComponentsLength; + } + components_[componentCount_++] = component; + component->setLocation(x, y); + component->repaint(); +} + +Component *Container::nextFocusHolder(Component *focusHolder, bool reverse) { + bool focusHolderFound = false; + return nextFocusHolder(focusHolder, reverse, &focusHolderFound); +} + +Component *Container::nextFocusHolder(Component *focusHolder, bool reverse, bool *focusHolderFound) { + for (int i = (reverse ? componentCount_ - 1 : 0); (reverse ? (i > 0) : (i < componentCount_)); (reverse ? i-- : i++)) { + Component *c = components_[i]; + if (c->isContainer()) { + Component *next = ((Container*) c)->nextFocusHolder(focusHolder, reverse, focusHolderFound); + if (next) { + return next; + } + } + else { + if (c->acceptsFocus()) { + if (!focusHolder || *focusHolderFound) { + return c; + } + else if (c == focusHolder) { + *focusHolderFound = true; + } + } + } + } + return NULL; +} + +bool Container::dirty() { + for (int i = 0; i < componentCount_; i++) { + if (components_[i]->dirty()) { + return true; + } + } + return false; +} + +bool Container::contains(Component *component) { + for (int i = 0; i < componentCount_; i++) { + Component *c = components_[i]; + if (c == component) { + return true; + } + else if (c->isContainer()) { + if (((Container*) c)->contains(component)) { + return true; + } + } + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// Component +//////////////////////////////////////////////////////////////////////////////// + +void Component::paint(Screen *screen) { + dirty_ = false; +} + +bool Component::dirty() { + return dirty_; +} + +//////////////////////////////////////////////////////////////////////////////// +// Label +//////////////////////////////////////////////////////////////////////////////// +Label::Label(const char *text) { + setSize(0, 1); + setText(text); + captured_ = false; + dirtyWidth_ = 0; +} + +void Label::paint(Screen *screen) { + Component::paint(screen); + + // Label does not accept focus, but Button, Checkbox and List are all + // subclasses that want to share the same text drawing system, so we + // just account for it here. + if (acceptsFocus()) { + if (screen->focusHolder() == this) { + if (captured_) { + screen->draw(x_, y_, ">"); + screen->draw(x_ + width_ + 1, y_, "<"); + } + else { + screen->draw(x_, y_, "<"); + screen->draw(x_ + width_ + 1, y_, ">"); + } + } + else { + screen->draw(x_, y_, "["); + screen->draw(x_ + width_ + 1, y_, "]"); + } + } + + screen->draw(x_ + (acceptsFocus() ? 1 : 0), y_, text_); + if (dirtyWidth_) { + for (int i = 0; i < dirtyWidth_ - width_; i++) { + screen->draw(x_ + width_ + i + (acceptsFocus() ? 2 : 0), y_, " "); + } + dirtyWidth_ = 0; + } +} + +void Label::setText(const char *text) { + text_ = (char*) text; + uint8_t newWidth = strlen(text); + if (newWidth < width_) { + dirtyWidth_ = width_; + } + width_ = newWidth; + repaint(); +} + +//////////////////////////////////////////////////////////////////////////////// +// Button +//////////////////////////////////////////////////////////////////////////////// +Button::Button(const char *text) : Label(text) { + setText(text); + pressed_ = false; +} + +void Button::update(Screen *screen) { + pressed_ = false; +} + +bool Button::handleInputEvent(int x, int y, bool selected, bool cancelled) { + pressed_ = selected; + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// Checkbox +//////////////////////////////////////////////////////////////////////////////// + +Checkbox::Checkbox() : Label(" ") { + checked_ = false; +} + +bool Checkbox::handleInputEvent(int x, int y, bool selected, bool cancelled) { + if (selected) { + checked_ = !checked_; + // Not James Bond. The 8th custom character location. By using a non-zero + // location we can still send it via a string, which means we can still + // be a Label instead of having a custom paint routine. + setText(checked_ ? "\007" : " "); + repaint(); + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +// List +//////////////////////////////////////////////////////////////////////////////// + +List::List(uint8_t maxItems) : Label(NULL) { + items_ = (char **) malloc(maxItems * (sizeof(char*))); + itemCount_ = 0; + selectedIndex_ = 0; + captured_ = false; +} + +void List::addItem(const char *item) { + items_[itemCount_++] = (char*) item; + if (text_ == NULL) { + setText(selectedItem()); + } +} + +void List::setSelectedIndex(uint8_t selectedIndex) { + selectedIndex_ = selectedIndex; + setText(selectedItem()); + repaint(); +} + +bool List::handleInputEvent(int x, int y, bool selected, bool cancelled) { + if (captured_ && y) { + if (y < 0) { + setSelectedIndex(max(selectedIndex_ + y, 0)); + } + else { + setSelectedIndex(min(selectedIndex_ + y, itemCount_ - 1)); + } + } + if (selected) { + captured_ = !captured_; + repaint(); + } + return captured_; +} + +//////////////////////////////////////////////////////////////////////////////// +// Input +//////////////////////////////////////////////////////////////////////////////// + +// TODO: trim incoming text and after each return, right justify the text +Input::Input(char *text) : Label((const char*) text) { + position_ = 0; + selecting_ = false; +} + +void Input::setText(char *text) { + Label::setText((const char *) text); + position_ = 0; + selecting_ = false; + repaint(); +} + +void Input::paint(Screen *screen) { + Label::paint(screen); + screen->setCursorVisible(captured_ && selecting_); + screen->setBlink(captured_ && !selecting_); + screen->setCursorLocation(x_ + position_ + 1, y_); +} + +bool Input::handleInputEvent(int x, int y, bool selected, bool cancelled) { + // If the input is captured and there has been a scroll event we're going to + // either change the position or change the selection. + if (captured_ && y) { + // If we're changing the selection, scroll through the character set. + if (selecting_) { + // TODO: replace this with a selectable character set that makes more + // sense + if (y < 0) { + text_[position_] = max(text_[position_] + y, ' '); + } + else { + text_[position_] = min(text_[position_] + y, '}'); + } + } + // Otherwise we are changing the position. If the position is moving before + // or after the field we release the input. + else { + position_ += y; + if (position_ < 0 || position_ >= width_) { + captured_ = false; + } + } + repaint(); + } + // If there has been a click we will either capture the input, + // start selection or end selection. + if (selected) { + // If input is captured we will start or end selection + // input. + if (captured_) { + selecting_ = !selecting_; + } + // Capture the input + else { + captured_ = true; + position_ = 0; + selecting_ = false; + } + repaint(); + } + return captured_; +} + +//////////////////////////////////////////////////////////////////////////////// +// ScrollContainer +//////////////////////////////////////////////////////////////////////////////// + +ScrollContainer::ScrollContainer(Screen *screen, uint8_t width, uint8_t height) { + setSize(width, height); + screen_ = screen; + clearLine = (char*) malloc(width + 1); + memset(clearLine, ' ', width); + clearLine[width] = NULL; + firstUpdateCompleted_ = false; +} + +ScrollContainer::~ScrollContainer() { + free(clearLine); +} + +void ScrollContainer::add(Component *component, int8_t x, int8_t y) { + Container::add(component, x, y); + if (firstUpdateCompleted_) { + // TODO: if the first update has already completed we need to update + // incoming components locations as they are added + } +} + +bool ScrollContainer::dirty() { + if (Container::dirty()) { + return true; + } + return scrollNeeded(); +} + +void ScrollContainer::update(Screen *screen) { + if (!firstUpdateCompleted_) { + offsetChildren(0, y_); + firstUpdateCompleted_ = true; + } +} + +void ScrollContainer::offsetChildren(int x, int y) { + for (int i = 0; i < componentCount_; i++) { + Component *c = components_[i]; + /* + Serial.print("Moving "); + Serial.print(c->description()); + Serial.print(" from "); + Serial.print(c->x(), DEC); + Serial.print(", "); + Serial.print(c->y(), DEC); + Serial.print(" to "); + Serial.print(c->x() + x, DEC); + Serial.print(", "); + Serial.println(c->y() + y, DEC); + */ + c->setLocation(c->x() + x, c->y() + y); + } +} + +bool ScrollContainer::scrollNeeded() { + // see if the focus holder has changed since the last check + Component *focusHolder = screen_->focusHolder(); + if (lastFocusHolder_ != focusHolder) { + // it has, so see if the new focus holder is one of ours + if (contains(focusHolder)) { + // it is, so we need to be sure it's visible, which means it's + // y position plus height has to be within our window of visibility + // our window of visbility is our y_ + row_ to y_ + row_ + height_ + uint8_t yStart = y_; + uint8_t yEnd = y_ + height_ - 1; + if (focusHolder->y() < yStart || focusHolder->y() > yEnd) { + // it is not currently visible, so we are dirty + return true; + } + } + } + return false; +} + +void ScrollContainer::paint(Screen *screen) { + Component::paint(screen); + if (scrollNeeded()) { + // we need to scroll the window + Component *focusHolder = screen_->focusHolder(); + // clear the window + for (int i = 0; i < height_; i++) { + screen->draw(x_, y_ + i, clearLine); + } + // set the new row_. if the new focus holder is below our currently + // visible area we want to increment the row the minimum amount to make it + // visible, and likewise if it is above, we want to decrement. + // TODO: These are calculated in scrollNeeded(), see if it would be better + // to reuse them somehow + uint8_t yStart = y_; + uint8_t yEnd = y_ + height_ - 1; + if (focusHolder->y() > yEnd) { + // if the component is below our visible window, increment the row count + // by the difference between the bottom visible row and the y position + // of the component + offsetChildren(0, yEnd - focusHolder->y()); + } + else { + offsetChildren(0, yStart - focusHolder->y()); + } + + lastFocusHolder_ = screen->focusHolder(); + + // tell all the children they need to be repainted. we will only paint + // the ones that are now visible + repaint(); + } + for (int i = 0; i < componentCount_; i++) { + Component *component = components_[i]; + if (component->dirty() && (component->y() >= y_) && (component->y() < y_ + height_)) { + component->paint(screen); + } + else { + component->clearDirty(); + } + } +} + diff --git a/ScreenUi.h b/ScreenUi.h new file mode 100644 index 0000000..4f486fd --- /dev/null +++ b/ScreenUi.h @@ -0,0 +1,294 @@ +/** + * A toolkit for building character based user interfaces on small displays. + * + * Copyright (c) 2012 Jason von Nieda + * This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 + * Unported License. To view a copy of this license, visit + * http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative + * Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. + */ + +#ifndef __ScreenUi_h__ +#define __ScreenUi_h__ + +#include + +//#define SCREENUI_DEBUG 1 + +#define min(a,b) ((a)<(b)?(a):(b)) +#define max(a,b) ((a)>(b)?(a):(b)) + +class Screen; + +class Component { + public: + Component() { x_ = y_ = width_ = height_ = 0; } + // Set the location on screen for this component. x and y are zero based, + // absolute character positions. + virtual void setLocation(int8_t x, int8_t y) { x_ = x; y_ = y;} + // Set the width and height this component. Most components will either + // require a width and height to be specified during creation or will + // adopt sane defaults based on their input data. + void setSize(uint8_t width, uint8_t height) { width_ = width; height_ = height; } + int8_t x() { return x_; } + int8_t y() { return y_; } + uint8_t width() { return width_; } + uint8_t height() { return height_; } + // Returns true if the component is willing to accept focus from the focus + // subsystem. For a component to receive input events it must be willing + // to accept focus. + virtual bool acceptsFocus() { return false; } + // The first step in the component update cycle. This is called by Screen + // during it's update cycle to allow each component to reset or set up + // any data that needs to be modified from the last update cycle. + virtual void update(Screen *screen) {} + // Called if the component has focus and is selected. x and y are delta + // since the last event. + // Returns true if the component wishes to remain selected. Returns false + // to give up selection. + virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled) { return false; } + // The final step in the component update cycle. Called by Screen to allow + // the component to draw itself on screen. It shoud generally draw itself + // at it's location and should not overflow it's size. + // Currently components are responsible for drawing their own focus + // indicator. This may change in the future. + // Component::paint() sets dirty to false and should be called by every + // subclass's paint method. + virtual void paint(Screen *screen); + // Returns true if the component is a container for other components. This + // is a shortcut so that we don't have to do RTTI when iterating over a + // list of components looking for containers. + virtual bool isContainer() { return false; } + // Returns true if the Component is marked dirty and needs to be painted + // on the next update. + virtual bool dirty(); + // Sets dirty to true for this Component, causing it to be painted during + // the next update. + // TODO refactor the two below into setDirty() + virtual void repaint() { dirty_ = true; } + virtual void clearDirty() { dirty_ = false; } + #ifdef SCREENUI_DEBUG + virtual char *description() { return "Component"; } + #endif + protected: + int8_t x_, y_; + uint8_t width_, height_; + bool dirty_; +}; + +// A Component that contains other Components. Users should not generally +// create instances of this class. +class Container : public Component { + public: + Container(); + ~Container(); + virtual void add(Component *component, int8_t x, int8_t y); + virtual void update(Screen *screen); + // Paints any dirty child components. + virtual void paint(Screen *screen); + virtual bool isContainer() { return true; } + // Returns true if any child components are dirty. + virtual bool dirty(); + // Sets dirty to true for all child components, causing them to be repainted + // during the next update. + virtual void repaint(); + #ifdef SCREENUI_DEBUG + virtual char *description() { return "Container"; } + #endif + virtual bool contains(Component *component); + protected: + Component *nextFocusHolder(Component *focusHolder, bool reverse); + Component *nextFocusHolder(Component *focusHolder, bool reverse, bool *focusHolderFound); + + Component **components_; + uint8_t componentsLength_; + uint8_t componentCount_; +}; + +// The main entry point into the ScreenUi system. A Screen instance represents +// a full screen of data on the display, including modifiable Components and +// provides methods for input and output. +// The user should create instances of Screen to manage a user interface and +// add() Components to the screen. +// When a Screen is ready to be displayed, the user should call update() in +// a loop. After each call to update(), Components can be queried for their +// data. +class Screen : public Container { + public: + Screen(uint8_t width, uint8_t height); + // Should be called regularly by the main program to update the Screen + // and process input. After each call to update(), each Component + // will have processed any input it received and will have updated it + // data. + virtual void update(); + // Returns the current focus holder for the Screen. The focus holder is + // the component that input events will be sent to. + Component *focusHolder() { return focusHolder_; } + // Sets the current focus holder. This can be used to set the default + // button on a screen before it is displayed, for instance. + void setFocusHolder(Component *focusHolder) { focusHolder_ = focusHolder; } + void setCursorLocation(uint8_t x, uint8_t y) { cursorX_ = x; cursorY_ = y; } + + #ifdef SCREENUI_DEBUG + virtual char *description() { return "Screen"; } + #endif + + // The following methods must be overridden by the user to provide + // hardware support + + // Get any changes in the input since the last call to update(); + virtual void getInputDeltas(int *x, int *y, bool *selected, bool *cancelled); + // Clear the screen + virtual void clear(); + virtual void createCustomChar(uint8_t slot, uint8_t *data); + virtual void draw(uint8_t x, uint8_t y, const char *text); + virtual void draw(uint8_t x, uint8_t y, uint8_t customChar); + virtual void setCursorVisible(bool visible); + virtual void setBlink(bool blink); + virtual void moveCursor(uint8_t x, uint8_t y); + + private: + bool cleared_; + Component *focusHolder_; + bool focusHolderSelected_; + bool annoyingBugWorkedAround_; + uint8_t cursorX_, cursorY_; +}; + +// A Component that displays static text at a specific position. +class Label : public Component { + public: + Label(const char *text); + virtual void setText(const char *text); + virtual void paint(Screen *screen); + #ifdef SCREENUI_DEBUG + virtual char *description() { return "Label"; } + #endif + protected: + char* text_; + bool captured_; + uint8_t dirtyWidth_; +}; + +// A Component that can receive focus and select events. If the Button has +// focus when the user presses the select button the Button's pressed() property +// is set indicating that the button was selected. +// Button is a subclass of Label and thus displays itself as text. +class Button : public Label { + public: + Button(const char *text); + virtual bool acceptsFocus() { return true; } + bool pressed() { return pressed_; } + virtual void update(Screen *screen); + virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); + #ifdef SCREENUI_DEBUG + virtual char *description() { return "Button"; } + #endif + private: + bool pressed_; +}; + +// A Component that displays either an on or off state. Clicking the component +// while it has focus causes it to change from on to off or vice-versa. +class Checkbox : public Label { + public: + Checkbox(); + bool checked() { return checked_; } + virtual bool acceptsFocus() { return true; } + virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); + #ifdef SCREENUI_DEBUG + virtual char *description() { return "Checkbox"; } + #endif + private: + bool checked_; +}; + +// A Component that allows the user to scroll through several choices and +// select one. When the List is selected, future scroll events will cause it +// to scroll through it's selections. A select sets the current item +// or a cancel resets the list to it's previously selected item and +// releases control. +class List : public Label { + public: + List(uint8_t maxItems); + void addItem(const char *item); + const char *selectedItem() { return items_[selectedIndex_]; } + uint8_t selectedIndex() { return selectedIndex_; } + void setSelectedIndex(uint8_t selectedIndex); + virtual bool acceptsFocus() { return true; } + virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); + #ifdef SCREENUI_DEBUG + virtual char *description() { return "List"; } + #endif + private: + char **items_; + uint8_t itemCount_; + uint8_t selectedIndex_; +}; + +// allows text input. Each character can be clicked to scroll through the alphabet. +class Input : public Label { + public: + Input(char *text); + virtual void setText(char *text); + virtual bool acceptsFocus() { return true; } + virtual void paint(Screen *screen); + virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); + #ifdef SCREENUI_DEBUG + virtual char *description() { return "Input"; } + #endif + protected: + int8_t position_; + bool selecting_; +}; + +// allows input of a floating point number. +class DecimalInput : public Input { +}; + +// allows input of an integer number with a certain base. +class IntegerInput : public Input { +}; + +// Component that allows the user to enter a time with up to three fields +// separated by semi-colons. e.g. 00, 00:00, 00:00:00 +class TimeInput : public Input { +}; + +// A Container that allows the user to scroll through any number of rows of +// Components. The ScrollContainer can be thought of as a Screen with the same +// width as the Container it is added to, and an unlimited height. +// Components that will be added to the ScrollContainer should have their +// location set relative to their position in the ScrollContainer, not +// the main Screen. +class ScrollContainer : public Container { + public: + // We require a reference to Screen because we need focus information + // during the dirty() check. This is a bit of a dirty hack, but it's + // better than adding this property to every other object or walking + // the tree to find it. + ScrollContainer(Screen *screen, uint8_t width, uint8_t height); + ~ScrollContainer(); + virtual void update(Screen *screen); + virtual void add(Component *component, int8_t x, int8_t y); + virtual void paint(Screen *screen); + virtual bool dirty(); + #ifdef SCREENUI_DEBUG + virtual char *description() { return "ScrollContainer"; } + #endif + private: + bool scrollNeeded(); + void offsetChildren(int x, int y); + + Component *lastFocusHolder_; + Screen *screen_; + char *clearLine; + bool firstUpdateCompleted_; +}; + +// A specialization of ScrollContainer that contains only Buttons and provides +// a simple API for managing the set of Buttons like a menu. +class Menu : public ScrollContainer { +}; + +#endif \ No newline at end of file