Initial import

master
Jason von Nieda 12 years ago
commit d51d778f92
  1. 532
      ScreenUi.cpp
  2. 294
      ScreenUi.h

@ -0,0 +1,532 @@
/**
* A toolkit for building character based user interfaces on small displays.
*
* Copyright (c) 2012 Jason von Nieda <jason@vonnieda.org>
* 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 <ScreenUi.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#ifdef SCREENUI_DEBUG
#include <WProgram.h>
#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();
}
}
}

@ -0,0 +1,294 @@
/**
* A toolkit for building character based user interfaces on small displays.
*
* Copyright (c) 2012 Jason von Nieda <jason@vonnieda.org>
* 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 <stdint.h>
//#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
Loading…
Cancel
Save