├── library.properties ├── README.md ├── ScreenUi.h └── ScreenUi.cpp /library.properties: -------------------------------------------------------------------------------- 1 | name=ScreenUi 2 | version=1.1.0 3 | author=Jason von Nieda 4 | maintainer=Jason von Nieda 5 | sentence=ScreenUi is a simple user interface library for character based LCDs like those commonly used with Arduinos. 6 | paragraph=It provides common user interface components such as labels, checkboxes, text fields, scrollable regions, spinners and buttons. 7 | category=Display 8 | url=https://github.com/vonnieda/ScreenUi 9 | architectures=* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ScreenUi makes it easy to build simple or complex character based user interfaces on small LCD displays like those commonly used with Arduinos. It offers a suite of widgets that you can use to build a screen and a set of simple ways to get data to and from the widgets. ScreenUi incorporates focus management, scrolling, text editing, input and output all in one easy to use library. 2 | 3 | Demonstration video: https://www.youtube.com/watch?v=fMjPy8N6kG0 4 | 5 | Here is a short Arduino sketch showing an example of how to use the library. Comments in the code explain how each piece works. 6 | 7 | ``` 8 | // A Sketch demonstrating ScreenUi using a 20x4 character LCD for output and 9 | // a rotary encoder with pushbutton for input. 10 | 11 | #include 12 | #include 13 | #include 14 | // This is the only required include for ScreenUi. 15 | // The others above are for a specific set of hardware input 16 | // and output. 17 | #include 18 | 19 | // Set up the rotary encoder and LCD. 20 | #define ENCODER_TYPE ALPS 21 | 22 | #define ENCA_PIN 3 23 | #define ENCB_PIN 2 24 | #define ENTER_PIN 1 25 | 26 | #define LCD_RS_PIN 4 27 | #define LCD_ENABLE_PIN 23 28 | #define LCD_DATA4_PIN 28 29 | #define LCD_DATA5_PIN 29 30 | #define LCD_DATA6_PIN 30 31 | #define LCD_DATA7_PIN 31 32 | 33 | #define LCD_BRIGHT_PIN 13 34 | #define LCD_CONTRAST_PIN 14 35 | 36 | LiquidCrystal lcd(LCD_RS_PIN, LCD_ENABLE_PIN, LCD_DATA4_PIN, LCD_DATA5_PIN, LCD_DATA6_PIN, LCD_DATA7_PIN); 37 | 38 | void setup() { 39 | // Everything here is for setting up the hardware. Nothing ScreenUi specific here. 40 | lcd.begin(20, 4); 41 | TCCR2B = 0x01; 42 | pinMode(LCD_BRIGHT_PIN, OUTPUT); 43 | pinMode(LCD_CONTRAST_PIN, OUTPUT); 44 | setBright(128); 45 | setContrast(98); 46 | Encoder.begin(ENCODER_TYPE, ENTER_PIN, ENCA_PIN, ENCB_PIN); 47 | Encoder.setWrap(true); 48 | Encoder.setMin(-10000); 49 | Encoder.setMax(10000); 50 | Encoder.setCount(0); 51 | Serial.begin(9600); 52 | Serial.println(); 53 | Serial.println(); 54 | Serial.println(); 55 | } 56 | 57 | // The loop() function is where all the ScreenUi magic happens. We'll create a 58 | // Screen, display it to the user, allow the user to interact and we'll take 59 | // input from the Components. 60 | void loop() { 61 | // Create a new Screen with width 20, height 4. 62 | Screen screen(20, 4); 63 | 64 | // Some static text that will be at the top of the screen. 65 | Label titleLabel("RGB Settings"); 66 | 67 | // An Input field and a Label to describe it. We pass in the text "0xffee" and 68 | // any changes the user makes to that text will be reflected in the address 69 | // variable. 70 | Label addressLabel("Address:"); 71 | char *address = "0xffee"; 72 | Input addressInput(address); 73 | 74 | // A List field and a Label to describe it. We add three items to the List for 75 | // the user to select from. 76 | Label colorLabel("Color:"); 77 | List colorList(7); 78 | colorList.addItem("Red"); 79 | colorList.addItem("Orange"); 80 | colorList.addItem("Yellow"); 81 | 82 | // A Checkbox field and a Label to describe it. 83 | Label rgbEnabledLabel("RGB Enabled:"); 84 | Checkbox rgbEnabledCheckbox; 85 | 86 | Label brightnessLabel("Brightness:"); 87 | Spinner brightnessSpinner(128, 0, 255, 1, true); 88 | 89 | Label contrastLabel("Contrast:"); 90 | Spinner contrastSpinner(128, 0, 255, 1, true); 91 | 92 | // A ScrollContainer to allow scrolling through multiple Components. Since our 93 | // screen is only 4 lines high but we want to show 5 lines of Components, we add 94 | // three of the widgets to the ScrollContainer. The ScrollContainer will appear 95 | // in the middle two lines of the display and allow the user to scroll through 96 | // as many Components as we like. 97 | // This line creates the ScrollContainer, passing the screen it will be attached 98 | // to and the width and height for the new ScrollContainer. 99 | ScrollContainer scrollContainer(&screen, screen.width(), 2); 100 | // Add the Components to the ScrollContainer, setting their position within it. 101 | scrollContainer.add(&addressLabel, 0, 0); 102 | scrollContainer.add(&addressInput, 8, 0); 103 | scrollContainer.add(&colorLabel, 0, 1); 104 | scrollContainer.add(&colorList, 6, 1); 105 | scrollContainer.add(&rgbEnabledLabel, 0, 2); 106 | scrollContainer.add(&rgbEnabledCheckbox, 12, 2); 107 | scrollContainer.add(&brightnessLabel, 0, 3); 108 | scrollContainer.add(&brightnessSpinner, 11, 3); 109 | scrollContainer.add(&contrastLabel, 0, 4); 110 | scrollContainer.add(&contrastSpinner, 9, 4); 111 | 112 | // A simple Cancel button. 113 | Button cancelButton("Cancel"); 114 | 115 | // A Simple Ok button. 116 | Button okButton("Ok"); 117 | 118 | // Add the title Label, the ScrollContainer and the Cancel and Ok buttons to the 119 | // screen. 120 | screen.add(&titleLabel, 0, 0); 121 | screen.add(&scrollContainer, 0, 1); 122 | screen.add(&cancelButton, 0, 3); 123 | screen.add(&okButton, 16, 3); 124 | 125 | // Start processing the Screen in a loop. 126 | while (1) { 127 | // screen.update() tells the Screen to display itself, accept input and manage 128 | // it's resources. 129 | screen.update(); 130 | // After calling screen.update(), all of the Components have been updated and drawn 131 | // to the Screen and their inputs are now available for querying. 132 | setBright(brightnessSpinner.intValue()); 133 | setContrast(contrastSpinner.intValue()); 134 | if (okButton.pressed()) { 135 | // Do some work 136 | } 137 | } 138 | } 139 | 140 | // The next 8 methods are required to be implemented by the user of ScreenUi. These 141 | // methods are what tie ScreenUi to your specific hardware for input and output. 142 | // In general, these will be very similar across platforms and can probably be copied 143 | // from one program to another and slightly modified. 144 | 145 | // User defined method that receives input from the input method. ScreenUi calls 146 | // this method during each update to see how the input state has changed since 147 | // the last update. ScreenUi expects the function to fill in the values 148 | // for x, y, selected and cancelled. 149 | // x and y are the number of inputs in either the x or y axis since the last call 150 | // to this method. The values can be positive or negative. A common control scheme 151 | // for a rotary encoder would be negative y for left, positive y for right. For 152 | // an input method consisting of the buttons on a NES control pad, for instance, might 153 | // have the D pad control x and y, the A button control selected and the B button 154 | // control cancelled. 155 | void Screen::getInputDeltas(int *x, int *y, bool *selected, bool *cancelled) { 156 | *x = 0; 157 | *y = Encoder.getDelta(); 158 | *selected = Encoder.ok(); 159 | *cancelled = Encoder.cancel(); 160 | Encoder.setCount(0); 161 | } 162 | 163 | // User defined method that clears the output device completely. 164 | void Screen::clear() { 165 | lcd.clear(); 166 | } 167 | 168 | // User defined method that creates a custom character in font memory. This is 169 | // currently used by the Checkbox Component to create a nice check mark. 170 | void Screen::createCustomChar(uint8_t slot, uint8_t *data) { 171 | lcd.createChar(slot, data); 172 | } 173 | 174 | // User defined method that draws the given text at the given x and y position. 175 | // The text should be drawn exactly as specified with no interpretation, scrolling 176 | // or wrapping. 177 | void Screen::draw(uint8_t x, uint8_t y, const char *text) { 178 | lcd.setCursor(x, y); 179 | lcd.print(text); 180 | } 181 | 182 | // User defined method that draws the given custom character at the given x 183 | // and y position. The custom character will be one specified to the 184 | // Screen::createCustomChar() method. 185 | void Screen::draw(uint8_t x, uint8_t y, uint8_t customChar) { 186 | lcd.setCursor(x, y); 187 | lcd.write(customChar); 188 | } 189 | 190 | // User defined method that turns the character cursor on or off. 191 | void Screen::setCursorVisible(bool visible) { 192 | visible ? lcd.cursor() : lcd.noCursor(); 193 | } 194 | 195 | // User defined method positions the character cursor. 196 | void Screen::moveCursor(uint8_t x, uint8_t y) { 197 | lcd.setCursor(x, y); 198 | } 199 | 200 | // User defined method that turns the blinking character on or off. 201 | void Screen::setBlink(bool blink) { 202 | blink ? lcd.blink() : lcd.noBlink(); 203 | } 204 | 205 | // Utility function for setting the brightness of the LCD. Not required for ScreenUi. 206 | void setBright(byte val) { 207 | analogWrite(LCD_BRIGHT_PIN, 255 - val); 208 | } 209 | 210 | // Utility function for setting the contrast of the LCD. Not required for ScreenUi. 211 | void setContrast(byte val) { 212 | analogWrite(LCD_CONTRAST_PIN, val); 213 | } 214 | ``` 215 | -------------------------------------------------------------------------------- /ScreenUi.h: -------------------------------------------------------------------------------- 1 | /** 2 | * ScreenUi 3 | * A toolkit for building character based user interfaces on small displays. 4 | * Copyright (c) 2012 Jason von Nieda 5 | * 6 | * This file is part of ScreenUi. 7 | * 8 | * ScreenUi is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * ScreenUi is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with ScreenUi. If not, see . 20 | */ 21 | 22 | #ifndef __ScreenUi_h__ 23 | #define __ScreenUi_h__ 24 | 25 | #include 26 | 27 | //#define SCREENUI_DEBUG 1 28 | 29 | #define min(a,b) ((a)<(b)?(a):(b)) 30 | #define max(a,b) ((a)>(b)?(a):(b)) 31 | 32 | class Screen; 33 | 34 | // Represents a set of characters that can be mapped to a continuous sequence 35 | // of integers starting with 0. 36 | class CharSet { 37 | public: 38 | // Returns true if the character set contains the given character. 39 | virtual bool contains(unsigned char ch) { return indexOf(ch) != -1; } 40 | // Returns the index in the character set for the given character. 41 | // Return -1 if the character is not part of the character set. 42 | virtual int indexOf(unsigned char ch); 43 | // Returns the character at the given index for the character set. 44 | // Return -1 if the index is not within the range of the character set. 45 | virtual int charAt(int index) = 0; 46 | // Returns the length of the character set. This method can be used 47 | // to determine what the maximum index for charAt() will be. 48 | virtual unsigned char size() = 0; 49 | }; 50 | 51 | // An implementation of CharSet that uses several ranges to determine it's 52 | // full character set 53 | class RangeCharSet : public CharSet { 54 | public: 55 | RangeCharSet(int rangeCount, ...); 56 | virtual ~RangeCharSet(); 57 | virtual int charAt(int index); 58 | virtual unsigned char size(); 59 | private: 60 | unsigned char rangeCount_; 61 | unsigned char *ranges_; 62 | }; 63 | 64 | extern RangeCharSet defaultCharSet; 65 | extern RangeCharSet floatingPointCharSet; 66 | 67 | class Component { 68 | public: 69 | Component() { x_ = y_ = width_ = height_ = 0; } 70 | // Set the location on screen for this component. x and y are zero based, 71 | // absolute character positions. 72 | virtual void setLocation(int8_t x, int8_t y) { x_ = x; y_ = y;} 73 | // Set the width and height this component. Most components will either 74 | // require a width and height to be specified during creation or will 75 | // adopt sane defaults based on their input data. 76 | void setSize(uint8_t width, uint8_t height) { width_ = width; height_ = height; } 77 | int8_t x() { return x_; } 78 | int8_t y() { return y_; } 79 | uint8_t width() { return width_; } 80 | uint8_t height() { return height_; } 81 | // Returns true if the component is willing to accept focus from the focus 82 | // subsystem. For a component to receive input events it must be willing 83 | // to accept focus. 84 | virtual bool acceptsFocus() { return false; } 85 | // The first step in the component update cycle. This is called by Screen 86 | // during it's update cycle to allow each component to reset or set up 87 | // any data that needs to be modified from the last update cycle. 88 | virtual void update(Screen *screen) {} 89 | // Called if the component has focus and is selected. x and y are delta 90 | // since the last event. 91 | // Returns true if the component wishes to remain selected. Returns false 92 | // to give up selection. 93 | virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled) { return false; } 94 | // The final step in the component update cycle. Called by Screen to allow 95 | // the component to draw itself on screen. It shoud generally draw itself 96 | // at it's location and should not overflow it's size. 97 | // Currently components are responsible for drawing their own focus 98 | // indicator. This may change in the future. 99 | // Component::paint() sets dirty to false and should be called by every 100 | // subclass's paint method. 101 | virtual void paint(Screen *screen); 102 | // Returns true if the component is a container for other components. This 103 | // is a shortcut so that we don't have to do RTTI when iterating over a 104 | // list of components looking for containers. 105 | virtual bool isContainer() { return false; } 106 | // Returns true if the Component is marked dirty and needs to be painted 107 | // on the next update. 108 | virtual bool dirty(); 109 | // Sets dirty to true for this Component, causing it to be painted during 110 | // the next update. 111 | // TODO refactor the two below into setDirty() 112 | virtual void repaint() { dirty_ = true; } 113 | virtual void clearDirty() { dirty_ = false; } 114 | #ifdef SCREENUI_DEBUG 115 | virtual char *description() { return "Component"; } 116 | #endif 117 | protected: 118 | int8_t x_, y_; 119 | uint8_t width_, height_; 120 | bool dirty_; 121 | }; 122 | 123 | // A Component that contains other Components. Users should not generally 124 | // create instances of this class. 125 | class Container : public Component { 126 | public: 127 | Container(); 128 | virtual ~Container(); 129 | virtual void add(Component *component, int8_t x, int8_t y); 130 | virtual void update(Screen *screen); 131 | // Paints any dirty child components. 132 | virtual void paint(Screen *screen); 133 | virtual bool isContainer() { return true; } 134 | // Returns true if any child components are dirty. 135 | virtual bool dirty(); 136 | // Sets dirty to true for all child components, causing them to be repainted 137 | // during the next update. 138 | virtual void repaint(); 139 | #ifdef SCREENUI_DEBUG 140 | virtual char *description() { return "Container"; } 141 | #endif 142 | virtual bool contains(Component *component); 143 | protected: 144 | Component *nextFocusHolder(Component *focusHolder, bool reverse); 145 | Component *nextFocusHolder(Component *focusHolder, bool reverse, bool *focusHolderFound); 146 | void offsetChildren(int x, int y); 147 | 148 | Component **components_; 149 | uint8_t componentsLength_; 150 | uint8_t componentCount_; 151 | bool firstUpdateCompleted_; 152 | }; 153 | 154 | // The main entry point into the ScreenUi system. A Screen instance represents 155 | // a full screen of data on the display, including modifiable Components and 156 | // provides methods for input and output. 157 | // The user should create instances of Screen to manage a user interface and 158 | // add() Components to the screen. 159 | // When a Screen is ready to be displayed, the user should call update() in 160 | // a loop. After each call to update(), Components can be queried for their 161 | // data. 162 | class Screen : public Container { 163 | public: 164 | Screen(uint8_t width, uint8_t height); 165 | // Should be called regularly by the main program to update the Screen 166 | // and process input. After each call to update(), each Component 167 | // will have processed any input it received and will have updated it 168 | // data. 169 | virtual void update(); 170 | // Returns the current focus holder for the Screen. The focus holder is 171 | // the component that input events will be sent to. 172 | Component *focusHolder() { return focusHolder_; } 173 | // Sets the current focus holder. This can be used to set the default 174 | // button on a screen before it is displayed, for instance. 175 | void setFocusHolder(Component *focusHolder) { focusHolder_ = focusHolder; } 176 | void setCursorLocation(uint8_t x, uint8_t y) { cursorX_ = x; cursorY_ = y; } 177 | 178 | #ifdef SCREENUI_DEBUG 179 | virtual char *description() { return "Screen"; } 180 | #endif 181 | 182 | // The following methods must be overridden by the user to provide 183 | // hardware support 184 | 185 | // Get any changes in the input since the last call to update(); 186 | virtual void getInputDeltas(int *x, int *y, bool *selected, bool *cancelled); 187 | // Clear the screen 188 | virtual void clear(); 189 | virtual void createCustomChar(uint8_t slot, uint8_t *data); 190 | virtual void draw(uint8_t x, uint8_t y, const char *text); 191 | virtual void draw(uint8_t x, uint8_t y, uint8_t customChar); 192 | virtual void setCursorVisible(bool visible); 193 | virtual void setBlink(bool blink); 194 | virtual void moveCursor(uint8_t x, uint8_t y); 195 | 196 | private: 197 | bool cleared_; 198 | Component *focusHolder_; 199 | bool focusHolderSelected_; 200 | bool annoyingBugWorkedAround_; 201 | uint8_t cursorX_, cursorY_; 202 | }; 203 | 204 | // A Component that displays static text at a specific position. 205 | class Label : public Component { 206 | public: 207 | Label(const char *text); 208 | virtual const char *text() { return (const char *) text_; } 209 | virtual void setText(const char *text); 210 | virtual void paint(Screen *screen); 211 | #ifdef SCREENUI_DEBUG 212 | virtual char *description() { return "Label"; } 213 | #endif 214 | protected: 215 | char* text_; 216 | bool captured_; 217 | uint8_t dirtyWidth_; 218 | }; 219 | 220 | // A Component that can receive focus and select events. If the Button has 221 | // focus when the user presses the select button the Button's pressed() property 222 | // is set indicating that the button was selected. 223 | // Button is a subclass of Label and thus displays itself as text. 224 | class Button : public Label { 225 | public: 226 | Button(const char *text); 227 | virtual bool acceptsFocus() { return true; } 228 | bool pressed() { return pressed_; } 229 | virtual void update(Screen *screen); 230 | virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); 231 | #ifdef SCREENUI_DEBUG 232 | virtual char *description() { return "Button"; } 233 | #endif 234 | private: 235 | bool pressed_; 236 | }; 237 | 238 | // A Component that displays either an on or off state. Clicking the component 239 | // while it has focus causes it to change from on to off or vice-versa. 240 | class Checkbox : public Label { 241 | public: 242 | Checkbox(); 243 | bool checked() { return checked_; } 244 | virtual bool acceptsFocus() { return true; } 245 | virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); 246 | #ifdef SCREENUI_DEBUG 247 | virtual char *description() { return "Checkbox"; } 248 | #endif 249 | private: 250 | bool checked_; 251 | }; 252 | 253 | // A Component that allows the user to scroll through several choices and 254 | // select one. When the List is selected, future scroll events will cause it 255 | // to scroll through it's selections. A select sets the current item 256 | // or a cancel resets the list to it's previously selected item and 257 | // releases control. 258 | class List : public Label { 259 | public: 260 | List(uint8_t maxItems); 261 | virtual ~List(); 262 | void addItem(const char *item); 263 | const char *selectedItem() { return items_[selectedIndex_]; } 264 | uint8_t selectedIndex() { return selectedIndex_; } 265 | void setSelectedIndex(uint8_t selectedIndex); 266 | virtual bool acceptsFocus() { return true; } 267 | virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); 268 | #ifdef SCREENUI_DEBUG 269 | virtual char *description() { return "List"; } 270 | #endif 271 | private: 272 | char **items_; 273 | uint8_t itemCount_; 274 | uint8_t selectedIndex_; 275 | }; 276 | 277 | // A Component that allows the user to scroll through a range of Integers 278 | // or floats. 279 | class Spinner : public Label { 280 | public: 281 | Spinner(int value, int low, int high, int increment, bool rollover); 282 | int intValue(); 283 | virtual bool acceptsFocus() { return true; } 284 | virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); 285 | #ifdef SCREENUI_DEBUG 286 | virtual char *description() { return "Spinner"; } 287 | #endif 288 | private: 289 | char buffer_[10]; 290 | int value_, low_, high_, increment_; 291 | bool rollover_; 292 | }; 293 | 294 | // allows text input. Each character can be clicked to scroll through the alphabet. 295 | class Input : public Label { 296 | public: 297 | Input(char *text); 298 | virtual void setText(char *text); 299 | virtual bool acceptsFocus() { return true; } 300 | virtual void paint(Screen *screen); 301 | virtual bool handleInputEvent(int x, int y, bool selected, bool cancelled); 302 | #ifdef SCREENUI_DEBUG 303 | virtual char *description() { return "Input"; } 304 | #endif 305 | void setCharSet(CharSet *charSet) { charSet_ = charSet; } 306 | CharSet *charSet() { return charSet_; } 307 | protected: 308 | int8_t position_; 309 | bool selecting_; 310 | CharSet *charSet_; 311 | }; 312 | 313 | // A Component that accepts input of a floating or fixed point decimal 314 | // number. 315 | class DecimalInput : public Input { 316 | }; 317 | 318 | // A Component that accepts input of an integer value with a given base. 319 | class IntegerInput : public Input { 320 | public: 321 | // Create a signed IntegerInput with the given value, width and base. 322 | // If width is 0 the width will be determined by the number of digits in 323 | // the incoming value, with one additional space for the negative. 324 | // If base is not specified, the default is base 10. 325 | IntegerInput(long value, unsigned char width = 0, unsigned char base = 10); 326 | // Create a signed IntegerInput with the given value, width and base. 327 | // If width is 0 the width will be determined by the number of digits in 328 | // the incoming value. 329 | // If base is not specified, the default is base 10. 330 | IntegerInput(unsigned long value, unsigned char width = 0, unsigned char base = 10); 331 | private: 332 | bool signed_; 333 | unsigned char base_; 334 | }; 335 | 336 | // Component that allows the user to enter a time with up to three fields 337 | // separated by semi-colons. e.g. 00, 00:00, 00:00:00 338 | class TimeInput : public Input { 339 | }; 340 | 341 | // A Container that allows the user to scroll through any number of rows of 342 | // Components. The ScrollContainer can be thought of as a Screen with the same 343 | // width as the Container it is added to, and an unlimited height. 344 | // Components that will be added to the ScrollContainer should have their 345 | // location set relative to their position in the ScrollContainer, not 346 | // the main Screen. 347 | class ScrollContainer : public Container { 348 | public: 349 | // We require a reference to Screen because we need focus information 350 | // during the dirty() check. This is a bit of a dirty hack, but it's 351 | // better than adding this property to every other object or walking 352 | // the tree to find it. 353 | ScrollContainer(Screen *screen, uint8_t width, uint8_t height); 354 | virtual ~ScrollContainer(); 355 | virtual void paint(Screen *screen); 356 | virtual bool dirty(); 357 | #ifdef SCREENUI_DEBUG 358 | virtual char *description() { return "ScrollContainer"; } 359 | #endif 360 | private: 361 | bool scrollNeeded(); 362 | 363 | Component *lastFocusHolder_; 364 | Screen *screen_; 365 | char *clearLine; 366 | }; 367 | 368 | // A specialization of ScrollContainer that contains only Buttons and provides 369 | // a simple API for managing the set of Buttons like a menu. 370 | class Menu : public ScrollContainer { 371 | }; 372 | 373 | #endif -------------------------------------------------------------------------------- /ScreenUi.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * ScreenUi 3 | * A toolkit for building character based user interfaces on small displays. 4 | * Copyright (c) 2012 Jason von Nieda 5 | * 6 | * This file is part of ScreenUi. 7 | * 8 | * ScreenUi is free software: you can redistribute it and/or modify 9 | * it under the terms of the GNU General Public License as published by 10 | * the Free Software Foundation, either version 3 of the License, or 11 | * (at your option) any later version. 12 | * 13 | * ScreenUi is distributed in the hope that it will be useful, 14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | * GNU General Public License for more details. 17 | * 18 | * You should have received a copy of the GNU General Public License 19 | * along with ScreenUi. If not, see . 20 | */ 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | #ifdef SCREENUI_DEBUG 29 | #include 30 | #endif 31 | 32 | void* operator new(size_t size) { return malloc(size); } 33 | void operator delete(void* ptr) { free(ptr); } 34 | 35 | // TODO: Change to PROGMEM 36 | uint8_t charCheckmark[] = {0, // B00000 37 | 0, // B00000 38 | 1, // B00001 39 | 2, // B00010 40 | 20, // B10100 41 | 8, // B01000 42 | 0, // B00000 43 | 0}; // B00000 44 | 45 | 46 | //////////////////////////////////////////////////////////////////////////////// 47 | // Screen 48 | //////////////////////////////////////////////////////////////////////////////// 49 | Screen::Screen(uint8_t width, uint8_t height) { 50 | setSize(width, height); 51 | cleared_ = false; 52 | focusHolder_ = NULL; 53 | focusHolderSelected_ = false; 54 | createCustomChar(7, charCheckmark); 55 | annoyingBugWorkedAround_ = false; 56 | } 57 | 58 | void Screen::update() { 59 | if (!cleared_) { 60 | clear(); 61 | cleared_ = true; 62 | } 63 | Container::update(this); 64 | int x, y; 65 | bool selected, cancelled; 66 | getInputDeltas(&x, &y, &selected, &cancelled); 67 | Component *oldFocusHolder = focusHolder_; 68 | if (x || y || selected || cancelled) { 69 | if (focusHolderSelected_) { 70 | focusHolderSelected_ = focusHolder_->handleInputEvent(x, y, selected, cancelled); 71 | } 72 | else { 73 | if (selected) { 74 | focusHolderSelected_ = focusHolder_->handleInputEvent(x, y, selected, cancelled); 75 | } 76 | else if (x || y) { 77 | // TODO: Make axis x or y configurable. 78 | // TODO: consider making the last widget in the screen the end of focus, 79 | // so that you don't cycle back to the top but instead lock at the end 80 | // and vice-verse. Maybe make this configurable. 81 | if (y > 0) { 82 | focusHolder_ = nextFocusHolder(focusHolder_, false); 83 | if (!focusHolder_) { 84 | focusHolder_ = nextFocusHolder(focusHolder_, false); 85 | } 86 | } 87 | else if (y < 0) { 88 | focusHolder_ = nextFocusHolder(focusHolder_, true); 89 | if (!focusHolder_) { 90 | focusHolder_ = nextFocusHolder(focusHolder_, true); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | if (focusHolder_ == NULL) { 97 | focusHolder_ = nextFocusHolder(focusHolder_, false); 98 | } 99 | if (oldFocusHolder != focusHolder_) { 100 | if (oldFocusHolder) { 101 | oldFocusHolder->repaint(); 102 | } 103 | focusHolder_->repaint(); 104 | } 105 | paint(this); 106 | moveCursor(cursorX_, cursorY_); 107 | 108 | // TODO: Bug I can't figure out. If we use the dirtyness system, the first paint 109 | // fails to draw anything to the screen and then subsequent ones don't get called 110 | // because they aren't dirty. The second paint works fine. 111 | // Adding repaint() here allows us to paint during the second loop which gets 112 | // everything into a state where it works great, but I can't figure out why. 113 | // It doesn't seem to have to do with timing or the clear(). 114 | if (!annoyingBugWorkedAround_) { 115 | repaint(); 116 | annoyingBugWorkedAround_ = true; 117 | } 118 | } 119 | 120 | //////////////////////////////////////////////////////////////////////////////// 121 | // Container 122 | //////////////////////////////////////////////////////////////////////////////// 123 | Container::Container() { 124 | components_ = NULL; 125 | componentsLength_ = 0; 126 | componentCount_ = 0; 127 | } 128 | 129 | Container::~Container() { 130 | if (components_) { 131 | free(components_); 132 | } 133 | } 134 | 135 | void Container::update(Screen *screen) { 136 | if (!firstUpdateCompleted_) { 137 | offsetChildren(0, y_); 138 | firstUpdateCompleted_ = true; 139 | } 140 | for (int i = 0; i < componentCount_; i++) { 141 | components_[i]->update(screen); 142 | } 143 | } 144 | 145 | void Container::paint(Screen *screen) { 146 | for (int i = 0; i < componentCount_; i++) { 147 | if (components_[i]->dirty()) { 148 | components_[i]->paint(screen); 149 | } 150 | } 151 | } 152 | 153 | void Container::repaint() { 154 | for (int i = 0; i < componentCount_; i++) { 155 | components_[i]->repaint(); 156 | } 157 | } 158 | 159 | void Container::add(Component *component, int8_t x, int8_t y) { 160 | if (!components_ || componentsLength_ <= componentCount_) { 161 | componentsLength_ = (componentsLength_ * 2) + 1; 162 | components_ = (Component**) realloc(components_, componentsLength_ * sizeof(Component*)); 163 | } 164 | components_[componentCount_++] = component; 165 | if (firstUpdateCompleted_) { 166 | // TODO: if the first update has already completed we need to update 167 | // incoming components locations as they are added 168 | } 169 | component->setLocation(x, y); 170 | component->repaint(); 171 | } 172 | 173 | void Container::offsetChildren(int x, int y) { 174 | for (int i = 0; i < componentCount_; i++) { 175 | Component *c = components_[i]; 176 | /* 177 | Serial.print("Moving "); 178 | Serial.print(c->description()); 179 | Serial.print(" from "); 180 | Serial.print(c->x(), DEC); 181 | Serial.print(", "); 182 | Serial.print(c->y(), DEC); 183 | Serial.print(" to "); 184 | Serial.print(c->x() + x, DEC); 185 | Serial.print(", "); 186 | Serial.println(c->y() + y, DEC); 187 | */ 188 | c->setLocation(c->x() + x, c->y() + y); 189 | } 190 | } 191 | 192 | Component *Container::nextFocusHolder(Component *focusHolder, bool reverse) { 193 | bool focusHolderFound = false; 194 | return nextFocusHolder(focusHolder, reverse, &focusHolderFound); 195 | } 196 | 197 | Component *Container::nextFocusHolder(Component *focusHolder, bool reverse, bool *focusHolderFound) { 198 | for (int i = (reverse ? componentCount_ - 1 : 0); (reverse ? (i > 0) : (i < componentCount_)); (reverse ? i-- : i++)) { 199 | Component *c = components_[i]; 200 | if (c->isContainer()) { 201 | Component *next = ((Container*) c)->nextFocusHolder(focusHolder, reverse, focusHolderFound); 202 | if (next) { 203 | return next; 204 | } 205 | } 206 | else { 207 | if (c->acceptsFocus()) { 208 | if (!focusHolder || *focusHolderFound) { 209 | return c; 210 | } 211 | else if (c == focusHolder) { 212 | *focusHolderFound = true; 213 | } 214 | } 215 | } 216 | } 217 | return NULL; 218 | } 219 | 220 | bool Container::dirty() { 221 | for (int i = 0; i < componentCount_; i++) { 222 | if (components_[i]->dirty()) { 223 | return true; 224 | } 225 | } 226 | return false; 227 | } 228 | 229 | bool Container::contains(Component *component) { 230 | for (int i = 0; i < componentCount_; i++) { 231 | Component *c = components_[i]; 232 | if (c == component) { 233 | return true; 234 | } 235 | else if (c->isContainer()) { 236 | if (((Container*) c)->contains(component)) { 237 | return true; 238 | } 239 | } 240 | } 241 | return false; 242 | } 243 | 244 | //////////////////////////////////////////////////////////////////////////////// 245 | // Component 246 | //////////////////////////////////////////////////////////////////////////////// 247 | 248 | void Component::paint(Screen *screen) { 249 | dirty_ = false; 250 | } 251 | 252 | bool Component::dirty() { 253 | return dirty_; 254 | } 255 | 256 | //////////////////////////////////////////////////////////////////////////////// 257 | // Label 258 | //////////////////////////////////////////////////////////////////////////////// 259 | Label::Label(const char *text) { 260 | setSize(0, 1); 261 | setText(text); 262 | captured_ = false; 263 | dirtyWidth_ = 0; 264 | } 265 | 266 | void Label::paint(Screen *screen) { 267 | Component::paint(screen); 268 | 269 | // Label does not accept focus, but Button, Checkbox and List are all 270 | // subclasses that want to share the same text drawing system, so we 271 | // just account for it here. 272 | if (acceptsFocus()) { 273 | if (screen->focusHolder() == this) { 274 | if (captured_) { 275 | screen->draw(x_, y_, ">"); 276 | screen->draw(x_ + width_ + 1, y_, "<"); 277 | } 278 | else { 279 | screen->draw(x_, y_, "<"); 280 | screen->draw(x_ + width_ + 1, y_, ">"); 281 | } 282 | } 283 | else { 284 | screen->draw(x_, y_, "["); 285 | screen->draw(x_ + width_ + 1, y_, "]"); 286 | } 287 | } 288 | 289 | screen->draw(x_ + (acceptsFocus() ? 1 : 0), y_, text_); 290 | if (dirtyWidth_) { 291 | for (int i = 0; i < dirtyWidth_ - width_; i++) { 292 | screen->draw(x_ + width_ + i + (acceptsFocus() ? 2 : 0), y_, " "); 293 | } 294 | dirtyWidth_ = 0; 295 | } 296 | } 297 | 298 | void Label::setText(const char *text) { 299 | text_ = (char*) text; 300 | uint8_t newWidth = strlen(text); 301 | if (newWidth < width_) { 302 | dirtyWidth_ = width_; 303 | } 304 | width_ = newWidth; 305 | repaint(); 306 | } 307 | 308 | //////////////////////////////////////////////////////////////////////////////// 309 | // Button 310 | //////////////////////////////////////////////////////////////////////////////// 311 | Button::Button(const char *text) : Label(text) { 312 | setText(text); 313 | pressed_ = false; 314 | } 315 | 316 | void Button::update(Screen *screen) { 317 | pressed_ = false; 318 | } 319 | 320 | bool Button::handleInputEvent(int x, int y, bool selected, bool cancelled) { 321 | pressed_ = selected; 322 | return false; 323 | } 324 | 325 | //////////////////////////////////////////////////////////////////////////////// 326 | // Checkbox 327 | //////////////////////////////////////////////////////////////////////////////// 328 | 329 | Checkbox::Checkbox() : Label(" ") { 330 | checked_ = false; 331 | } 332 | 333 | bool Checkbox::handleInputEvent(int x, int y, bool selected, bool cancelled) { 334 | if (selected) { 335 | checked_ = !checked_; 336 | // Not James Bond. The 8th custom character location. By using a non-zero 337 | // location we can still send it via a string, which means we can still 338 | // be a Label instead of having a custom paint routine. 339 | setText(checked_ ? "\007" : " "); 340 | repaint(); 341 | } 342 | return false; 343 | } 344 | 345 | //////////////////////////////////////////////////////////////////////////////// 346 | // List 347 | //////////////////////////////////////////////////////////////////////////////// 348 | 349 | List::List(uint8_t maxItems) : Label(NULL) { 350 | items_ = (char **) malloc(maxItems * (sizeof(char*))); 351 | itemCount_ = 0; 352 | selectedIndex_ = 0; 353 | captured_ = false; 354 | } 355 | 356 | List::~List() { 357 | free(items_); 358 | } 359 | 360 | void List::addItem(const char *item) { 361 | items_[itemCount_++] = (char*) item; 362 | if (text_ == NULL) { 363 | setText(selectedItem()); 364 | } 365 | } 366 | 367 | void List::setSelectedIndex(uint8_t selectedIndex) { 368 | selectedIndex_ = selectedIndex; 369 | setText(selectedItem()); 370 | repaint(); 371 | } 372 | 373 | bool List::handleInputEvent(int x, int y, bool selected, bool cancelled) { 374 | if (captured_ && y) { 375 | if (y < 0) { 376 | setSelectedIndex(max(selectedIndex_ + y, 0)); 377 | } 378 | else { 379 | setSelectedIndex(min(selectedIndex_ + y, itemCount_ - 1)); 380 | } 381 | } 382 | if (selected) { 383 | captured_ = !captured_; 384 | repaint(); 385 | } 386 | return captured_; 387 | } 388 | 389 | //////////////////////////////////////////////////////////////////////////////// 390 | // Spinner 391 | //////////////////////////////////////////////////////////////////////////////// 392 | 393 | Spinner::Spinner(int value, int low, int high, int increment, bool rollover) : Label(NULL) { 394 | value_ = value; 395 | low_ = low; 396 | high_ = high; 397 | increment_ = increment; 398 | rollover_ = rollover; 399 | sprintf(buffer_, "%d", value_); 400 | setText(buffer_); 401 | } 402 | 403 | int Spinner::intValue() { 404 | return value_; 405 | } 406 | 407 | bool Spinner::handleInputEvent(int x, int y, bool selected, bool cancelled) { 408 | if (captured_ && y) { 409 | value_ += (y * increment_); 410 | if (value_ < low_) { 411 | value_ = rollover_ ? high_ : low_; 412 | } 413 | else if (value_ > high_) { 414 | value_ = rollover_ ? low_ : high_; 415 | } 416 | sprintf(buffer_, "%d", value_); 417 | setText(buffer_); 418 | repaint(); 419 | } 420 | if (selected) { 421 | captured_ = !captured_; 422 | repaint(); 423 | } 424 | } 425 | 426 | //////////////////////////////////////////////////////////////////////////////// 427 | // Input 428 | //////////////////////////////////////////////////////////////////////////////// 429 | 430 | // TODO: trim incoming text and after each return, right justify the text 431 | Input::Input(char *text) : Label((const char*) text) { 432 | position_ = 0; 433 | selecting_ = false; 434 | charSet_ = &defaultCharSet; 435 | } 436 | 437 | void Input::setText(char *text) { 438 | Label::setText((const char *) text); 439 | position_ = 0; 440 | selecting_ = false; 441 | repaint(); 442 | } 443 | 444 | void Input::paint(Screen *screen) { 445 | Label::paint(screen); 446 | screen->setCursorVisible(captured_ && selecting_); 447 | screen->setBlink(captured_ && !selecting_); 448 | screen->setCursorLocation(x_ + position_ + 1, y_); 449 | } 450 | 451 | bool Input::handleInputEvent(int x, int y, bool selected, bool cancelled) { 452 | // If the input is captured and there has been a scroll event we're going to 453 | // either change the position or change the selection. 454 | if (captured_ && y) { 455 | // If we're changing the selection, scroll through the character set. 456 | if (selecting_) { 457 | // TODO: replace this with a selectable character set that makes more 458 | // sense 459 | if (y < 0) { 460 | text_[position_] = charSet_->charAt(max(charSet_->indexOf(text_[position_]) + y, 0)); 461 | } 462 | else { 463 | text_[position_] = charSet_->charAt(min(charSet_->indexOf(text_[position_]) + y, charSet_->size() - 1)); 464 | } 465 | } 466 | // Otherwise we are changing the position. If the position is moving before 467 | // or after the field we release the input. 468 | else { 469 | position_ += y; 470 | if (position_ < 0 || position_ >= width_) { 471 | captured_ = false; 472 | } 473 | } 474 | repaint(); 475 | } 476 | // If there has been a click we will either capture the input, 477 | // start selection or end selection. 478 | if (selected) { 479 | // If input is captured we will start or end selection 480 | // input. 481 | if (captured_) { 482 | selecting_ = !selecting_; 483 | } 484 | // Capture the input 485 | else { 486 | captured_ = true; 487 | position_ = 0; 488 | selecting_ = false; 489 | } 490 | repaint(); 491 | } 492 | return captured_; 493 | } 494 | 495 | 496 | //////////////////////////////////////////////////////////////////////////////// 497 | // IntegerInput 498 | //////////////////////////////////////////////////////////////////////////////// 499 | IntegerInput::IntegerInput(long value, unsigned char width, unsigned char base) : Input(NULL) { 500 | signed_ = true; 501 | base_ = base; 502 | if (!width) { 503 | // count the digits 504 | } 505 | } 506 | 507 | IntegerInput::IntegerInput(unsigned long value, unsigned char width, unsigned char base) : Input(NULL) { 508 | signed_ = false; 509 | base_ = base; 510 | if (!width) { 511 | // count the digits 512 | } 513 | } 514 | 515 | //////////////////////////////////////////////////////////////////////////////// 516 | // ScrollContainer 517 | //////////////////////////////////////////////////////////////////////////////// 518 | 519 | ScrollContainer::ScrollContainer(Screen *screen, uint8_t width, uint8_t height) { 520 | setSize(width, height); 521 | screen_ = screen; 522 | clearLine = (char*) malloc(width + 1); 523 | memset(clearLine, ' ', width); 524 | clearLine[width] = NULL; 525 | firstUpdateCompleted_ = false; 526 | } 527 | 528 | ScrollContainer::~ScrollContainer() { 529 | free(clearLine); 530 | } 531 | 532 | bool ScrollContainer::dirty() { 533 | if (Container::dirty()) { 534 | return true; 535 | } 536 | return scrollNeeded(); 537 | } 538 | 539 | bool ScrollContainer::scrollNeeded() { 540 | // see if the focus holder has changed since the last check 541 | Component *focusHolder = screen_->focusHolder(); 542 | if (lastFocusHolder_ != focusHolder) { 543 | // it has, so see if the new focus holder is one of ours 544 | if (contains(focusHolder)) { 545 | // it is, so we need to be sure it's visible, which means it's 546 | // y position plus height has to be within our window of visibility 547 | // our window of visbility is our y_ + row_ to y_ + row_ + height_ 548 | uint8_t yStart = y_; 549 | uint8_t yEnd = y_ + height_ - 1; 550 | if (focusHolder->y() < yStart || focusHolder->y() > yEnd) { 551 | // it is not currently visible, so we are dirty 552 | return true; 553 | } 554 | } 555 | } 556 | return false; 557 | } 558 | 559 | void ScrollContainer::paint(Screen *screen) { 560 | Component::paint(screen); 561 | if (scrollNeeded()) { 562 | // we need to scroll the window 563 | Component *focusHolder = screen_->focusHolder(); 564 | // clear the window 565 | for (int i = 0; i < height_; i++) { 566 | screen->draw(x_, y_ + i, clearLine); 567 | } 568 | // set the new row_. if the new focus holder is below our currently 569 | // visible area we want to increment the row the minimum amount to make it 570 | // visible, and likewise if it is above, we want to decrement. 571 | // TODO: These are calculated in scrollNeeded(), see if it would be better 572 | // to reuse them somehow 573 | uint8_t yStart = y_; 574 | uint8_t yEnd = y_ + height_ - 1; 575 | if (focusHolder->y() > yEnd) { 576 | // if the component is below our visible window, increment the row count 577 | // by the difference between the bottom visible row and the y position 578 | // of the component 579 | offsetChildren(0, yEnd - focusHolder->y()); 580 | } 581 | else { 582 | offsetChildren(0, yStart - focusHolder->y()); 583 | } 584 | 585 | lastFocusHolder_ = screen->focusHolder(); 586 | 587 | // tell all the children they need to be repainted. we will only paint 588 | // the ones that are now visible 589 | repaint(); 590 | } 591 | for (int i = 0; i < componentCount_; i++) { 592 | Component *component = components_[i]; 593 | if (component->dirty() && (component->y() >= y_) && (component->y() < y_ + height_)) { 594 | component->paint(screen); 595 | } 596 | else { 597 | component->clearDirty(); 598 | } 599 | } 600 | } 601 | 602 | //////////////////////////////////////////////////////////////////////////////// 603 | // CharSet 604 | //////////////////////////////////////////////////////////////////////////////// 605 | 606 | int CharSet::indexOf(unsigned char ch) { 607 | for (int i = 0; i < size(); i++) { 608 | if (charAt(i) == ch) { 609 | return i; 610 | } 611 | } 612 | return -1; 613 | } 614 | 615 | //////////////////////////////////////////////////////////////////////////////// 616 | // RangeCharSet 617 | //////////////////////////////////////////////////////////////////////////////// 618 | 619 | RangeCharSet::RangeCharSet(int rangeCount, ...) { 620 | rangeCount_ = (unsigned char) rangeCount; 621 | ranges_ = (unsigned char *) malloc(sizeof(unsigned char) * rangeCount * 2); 622 | va_list argp; 623 | va_start(argp, rangeCount); 624 | for (int i = 0; i < rangeCount; i++) { 625 | ranges_[i * 2] = (unsigned char) va_arg(argp, int); 626 | ranges_[i * 2 + 1] = (unsigned char) va_arg(argp, int); 627 | } 628 | va_end(argp); 629 | } 630 | 631 | RangeCharSet::~RangeCharSet() { 632 | free(ranges_); 633 | } 634 | 635 | int RangeCharSet::charAt(int index) { 636 | // determine which range the index falls within by iterating the ranges 637 | // and then use that plus the index to determine the character 638 | int currentIndex = 0; 639 | for (int i = 0; i < rangeCount_; i++) { 640 | if (index >= currentIndex && index <= currentIndex + (ranges_[i * 2 + 1] - ranges_[i * 2])) { 641 | unsigned char ch = (unsigned char) (ranges_[i * 2] + (index - currentIndex)); 642 | return (int) ch; 643 | } 644 | else { 645 | currentIndex += (ranges_[i * 2 + 1] - ranges_[i * 2]) + 1; 646 | } 647 | } 648 | return -1; 649 | } 650 | 651 | unsigned char RangeCharSet::size() { 652 | unsigned char size = 0; 653 | for (int i = 0; i < rangeCount_; i++) { 654 | size += (ranges_[i * 2 + 1] - ranges_[i * 2]) + 1; 655 | } 656 | return size; 657 | } 658 | 659 | RangeCharSet defaultCharSet(8, 660 | 32, 32, // space 661 | 65, 90, // capital letters 662 | 97, 122, // lowercase letters 663 | 48, 57, // digits 664 | 33, 47, // special chars 665 | 58, 64, // special chars 666 | 91, 96, // special chars 667 | 123, 126 // special chars 668 | ); 669 | 670 | RangeCharSet floatingPointCharSet(4, 671 | 32, 32, // space 672 | 48, 57, // digits 673 | 46, 46, // period 674 | 45, 45 // negative 675 | ); 676 | 677 | --------------------------------------------------------------------------------