diff --git a/assets/texts/manual/colorchord.md b/assets/texts/manual/colorchord.md new file mode 100644 index 00000000..ef05686b --- /dev/null +++ b/assets/texts/manual/colorchord.md @@ -0,0 +1,7 @@ +# \c540Colorchord\C + +\lC\c505_The Swadge's sound-reactive LEDs make any music a party!_\C\L + +Use Left and Right or Select to select options like microphone gain, LED brightness, and LED output style. + +Use Up, Down, A, B, and Start to cycle through options. diff --git a/assets/texts/manual/dice_roller.md b/assets/texts/manual/dice_roller.md new file mode 100644 index 00000000..8a985248 --- /dev/null +++ b/assets/texts/manual/dice_roller.md @@ -0,0 +1,5 @@ +# \c540Dice Roller\C + +\lC\c505_Roll Dice! (That's it)_\C\L + +Roll Dice! Use Up and Down to adjust values, and Left and Right to select either number of dice or number of sides. '6d20' would roll 6 dice with 20 sides each. Press A or B to reroll. Your recent rolls will be visible in the History panel. diff --git a/assets/texts/manual/donut_jump.md b/assets/texts/manual/donut_jump.md new file mode 100644 index 00000000..6b563ec6 --- /dev/null +++ b/assets/texts/manual/donut_jump.md @@ -0,0 +1,9 @@ +# \c540Donut Jump\C + +\lC\c505_Jump your way to a sweet victory!_\C\L + +Use the Directional Buttons to move King Donut from block to block to change the blocks' colors. Once all blocks have changed from blue to gold, the round is complete. Build a combo by only jumping on blue blocks without being hit by the Evil Eclair or Devil Donut. Not all blocks are good for landing on, either. + +The red blocks are just as devious as the enemies and will cause you to lose a life. The purple blocks will eliminate your combo, but they are also the only spaces that enemies dare not travel. + +Keep your eyes open for the Can of Cowabunga, which gives you momentary relief as the enemies won't chase you while it is active. But, be aware they can still harm you if you try and share the same space. diff --git a/assets/texts/manual/flyin_donut.md b/assets/texts/manual/flyin_donut.md new file mode 100644 index 00000000..e4e61e3a --- /dev/null +++ b/assets/texts/manual/flyin_donut.md @@ -0,0 +1,9 @@ +# \c540Flyin Donut\C + +\lC\c505_Fly through a reimagined Gaylord Atrium (and collect beans!)_\C\L + +Use the up, down, left, and right buttons to pilot the Bean Blaster. The Bean Blaster is a high-energy ship, so it's always moving forward. Pressing the action button will push it even faster, at the cost of maneuverability. + +If you don't like the way it's handling, you can invert the Y axis from the main menu. + +You must go through all donuts and through the gazebo at the end to complete the Atrium Course. Free Flight mode is also available if you just want to sightsee! diff --git a/assets/texts/manual/gamepad.md b/assets/texts/manual/gamepad.md new file mode 100644 index 00000000..ca284bcc --- /dev/null +++ b/assets/texts/manual/gamepad.md @@ -0,0 +1,11 @@ +# \c235Tools\C + +--- + +# \c540Gamepad\C + +\lC\c505_Use the Swadge as a USB Gamepad!_\C\L + +Choose your Gamepad type, plug it into a PC or your game console of choice with a USB C-A or C-C cable (not included), and game on! All of the buttons, touch buttons, Touchpad analog value, and accelerometer data are sent to the host PC. + +In Switch mode, holding Down and pressing Start is equivalent to pressing the console's 'Home' button. Holding Down and pressing Select will capture a screenshot on the console. diff --git a/assets/texts/manual/jukebox.md b/assets/texts/manual/jukebox.md new file mode 100644 index 00000000..1bbbbe95 --- /dev/null +++ b/assets/texts/manual/jukebox.md @@ -0,0 +1,7 @@ +# \c540Jukebox\C + +\lC\c505_Listen to your favorite Swadge music and SFX anytime!_\C\L + +Once you enter the Jukebox mode, pick either Music or SFX on the menu, and you can get started. Use the Directional Buttons to find that underground theme you liked from Swadge Land, or give a listen to unused and hidden songs! + +To entertain your eyes while we entertain your ears, you can pick from a selection of our favorite LED animations from the Light Dances mode. diff --git a/assets/texts/manual/light_dances.md b/assets/texts/manual/light_dances.md new file mode 100644 index 00000000..6736269f --- /dev/null +++ b/assets/texts/manual/light_dances.md @@ -0,0 +1,7 @@ +# \c540Light Dances\C + +\lC\c505_Show off your Swadge with your favorite LED animations!_\C\L + +Choose your animation with Left and Right, adjust the brightness with Up and Down, and adjust the speed with the X and Y touch buttons. + +The display will turn off after a few seconds and can be awakened with any button. diff --git a/assets/texts/manual/main_menu.md b/assets/texts/manual/main_menu.md new file mode 100644 index 00000000..474df9fe --- /dev/null +++ b/assets/texts/manual/main_menu.md @@ -0,0 +1,13 @@ +# \c540Main Menu\C + +\lC\c505_Every journey has a beginning!_\C\L + +There are three main categories - **Tools**, **Music**, and **Games** - and you can find them all here! + +To return to the Main Menu +at any time, hold down +**Select** + **Start** for a few seconds. + +The Settings menu lets you turn music and sound effects on and off, set LED brightness, screensaver timeout, and mic gain. + +Oh, and check out the credits! \ No newline at end of file diff --git a/assets/texts/manual/manual_intro.md b/assets/texts/manual/manual_intro.md new file mode 100644 index 00000000..ef3dcfe2 --- /dev/null +++ b/assets/texts/manual/manual_intro.md @@ -0,0 +1,7 @@ +# \c235Meet Your Swadge\C + +--- + +More info, tips, and development resources are available at \c505~https://swadge.com/super2023/~\C + +Use Left and Right to navigate pages. Press Select to open the Table of Contents. diff --git a/assets/texts/manual/mfpaint.md b/assets/texts/manual/mfpaint.md new file mode 100644 index 00000000..e050501e --- /dev/null +++ b/assets/texts/manual/mfpaint.md @@ -0,0 +1,11 @@ +# \c540MFPaint\C + +\lC\c505_Create your very own 8-bit art masterpieces!_\C\L + +In Draw mode, the Directional Buttons move the cursor around the canvas. Press or hold A to draw or select points, depending on which tool is selected. Press B to swap the foreground and background colors. Tap X or swipe right on the touchpad to increases brush size; tap Y or swipe left on the touchpad to decrease brush size. + +Press and hold the Touchpad to enable select mode, where Up and Down change the foreground color, and Left and Right change the active tool. Releasing the touchpad confirms and exits select mode. Press Start to toggle the save menu. Here, you can save and load from four slots, reset the canvas, edit the palette, and exit Draw mode. + +Once you've created your masterpiece, it can be viewed in all its glory in the Gallery. Cycle through all your creations or just show off your favorite, accompanied by your favorite LED dance. In the Sharing menu, you can wirelessly send and receive your creations! + +To send, select Share, pick the artwork to share, and press A. The Swadge will search for a nearby receiver, and automatically send the artwork. To receive, select Receive, and the Swadge will search for a nearby sender, and receive the artwork. After receiving, you may select a slot with Left and Right, and save with A, or press B to exit without saving. diff --git a/assets/texts/manual/pi_cross.md b/assets/texts/manual/pi_cross.md new file mode 100644 index 00000000..45b263ca --- /dev/null +++ b/assets/texts/manual/pi_cross.md @@ -0,0 +1,15 @@ +# \c540Pi-Cross\C + +\lC\c505_Solve nonogram puzzles and reveal pixel art!_\C\L + +Correctly fill in the grid according to the clues. Set all spaces correctly to win and reveal the pixel art! + +The numbers to the left of a row are the clue for that row, and the numbers above a column are the clues for that column. Each number in a clue represents a connected series of blocks. There must be at least one gap between the blocks. + +Turning on "Guides" in the options menu will tint the selected row/column and show the width or height of the group of blocks the cursor is hovering over, on the right/bottom. + +There are also yellow lines every 5 squares to help you count. + +Move the blue input square with the d-pad. Press A to toggle spaces as filled. Press B to mark which spaces are empty. Press START to reset the counter (top-left, below the coordinates). Press SELECT to return to the level selection screen and save your progress. + +None of the puzzles in pi-cross require guessing in order to solve. The solution can always be logically deduced from the clues, and every puzzle has a single unique solution. diff --git a/assets/texts/manual/super_swadge_land.md b/assets/texts/manual/super_swadge_land.md new file mode 100644 index 00000000..4440aad5 --- /dev/null +++ b/assets/texts/manual/super_swadge_land.md @@ -0,0 +1,43 @@ +# \c540Super Swadge Land\C + +\lC\c505_16 levels of conventional 2D platforming action!_\C\L + +Make your way across Swadge Land's 16 distinct areas, each full of secrets and danger! Do it with style and see if you can rack up a high score! Seek out the power of Music and Gaming and you might just become unstoppable! + +The green LEDs underneath your hands indicate your Hit Points: + +Bottom LEDs: 1 HP +Middle LEDs: 2 HP +Top LEDs: 3 (MAX) HP + +If you touch an enemy without stomping them from above, you will lose 1 HP. Lose all HP, or fall off the bottom of the screen, and you'll lose a life. + +Watch that timer, denoted by "T" in the top right corner of the screen. You guessed it! Lose a life when it reaches 0. + +Every level has a Checkpoint Flag. Touch it and you will restart from its location whenever you lose a life. + +The shining red Container Blocks hide useful items. Hit them from any direction to release their contents! + +Find a "Gaming" or "Music" power-up to increase your HP. + +Touch a Warp Vortex and you'll be taken either to a bonus room or to the next part of the level. + +Some Container Blocks may be invisible until activated. But you might just uncover a 1UP Heart for finding one... + +Coins will grant an extra life for each 100 coins collected. Your current coin count is denoted by "C" in the top of the screen. + +Brick Blocks can contain anything a Container Block can, but otherwise can be broken from the underside. + +Bounce Blocks are just that; they will bounce you forcefully in the opposite direction that you touch them. Hold A while landing on top of them to bounce higher. + +When you have max HP, you can press B to fire Squarewave Bolts. They're slightly unwieldy, but can get you out of a bind! + +Game over? You can use the "Level Select" option on the title menu to continue from the highest level you've reached. + +There are 3 types of enemies that seek your destruction! Each type appears in 3 color variants of varying viciousness. + +All enemy types can be defeated by stomping on them from above, zapping them with a Squarewave Bolt, or bumping a Container/Brick/Bounce Block into them. + +Most actions you take will advance the score multiplier. Keep the multiplier alive by chaining actions together. This is key to getting a high score. + +At the end of each level is a grassy field marked with lines and letter grades. Go for big points at the end of every level by jumping and landing on the area with the best letter grade! Grades range from D (smallest bonus) to A, then Star (huge bonus). diff --git a/assets/texts/manual/swadge_bros.md b/assets/texts/manual/swadge_bros.md new file mode 100644 index 00000000..3cffad2b --- /dev/null +++ b/assets/texts/manual/swadge_bros.md @@ -0,0 +1,17 @@ +# \c235Games\C + +--- + +# \c540Swadge Bros\C + +\lC\c505_MAGFest's All-Stars in an all-out Brawl!_\C\L + +Swadge Bros has three main modes: Multiplayer (wireless or wired), VS. CPU, and Home Run Contest. + +The controls are the same for all modes. The Directional Buttons move the fighter, the A button jumps, and the B button attacks. The direction of the Directional Buttons influences the attack which is performed. There are five attacks on the ground: neutral, forward tilt, dash, up tilt, and down tilt. There are five more attacks in the air: neutral air, forward air, back air, up air, and down air. During recovery, if a fighter has no jumps left, they will get a bonus 'ledge jump.' + +In the multiplayer modes, either hold two Swadges close to each other to pair wirelessly, or connect two with a USB C-C cable. Once connected, select your fighter. One Swadge, randomly chosen, will select the stage. Duke it out until one fighter is left standing! Records are kept for multiplayer matches. + +In VS. CPU, practice against a CPU opponent. You get to pick your fighter, the CPU fighter, and the stage. Records are not kept for VS. CPU matches. + +In Home Run Contest, try to whack the Sandbag as far off to the right as you can. Rack up tons of damage and give it a good hit after the stage barrier has dropped! Records are kept for the Home Run Contest. diff --git a/assets/texts/manual/techno_slide_whistle.md b/assets/texts/manual/techno_slide_whistle.md new file mode 100644 index 00000000..a409b11c --- /dev/null +++ b/assets/texts/manual/techno_slide_whistle.md @@ -0,0 +1,11 @@ +# \c540TechnoSlideWhistle\C + +\lC\c505_We put a synth. a slide whistle. and fun rhythms in a blender!_\C\L + +To make music, tilt your Swadge left and right like a steering wheel or pirate ship wheel and press A. It'll play notes as long as A is held down. You can also touch the Touchpad to play notes instead of tilting and pressing A! + +If you press and hold B while playing something, the base note will be held until you release B. Use this to jump from one note or octave to another. + +Once you get the hang of it, give some rhythms a try. These play as long as A or the Touchpad is held. Some of them are even arpeggiated. + +Cycle through the different musical scales and tempos too, and see what you can create! diff --git a/assets/texts/manual/tiltrads_color.md b/assets/texts/manual/tiltrads_color.md new file mode 100644 index 00000000..b9d21ebb --- /dev/null +++ b/assets/texts/manual/tiltrads_color.md @@ -0,0 +1,5 @@ +# \c540Tiltrads Color\C + +\lC\c505_A motion-controlled spin on a classic puzzle game!_\C\L + +Fill rows to clear them by aligning falling tetrads. Clear up to 4 rows simultaneously for score bonuses. Tilting the Swadge left and right moves the active tetrad left and right. Pressing A/B rotates the active tetrad. Pressing Down soft-drops the active tetrad, increasing its fall speed. Pressing Up immediately hard-drops and locks in the active tetrad. Soft-dropping and hard-dropping tetrads provide score bonuses. The speed of falling tetrads increases over time. If the board fills up to the point that no new tetrad can be placed, the game is over. Compete for the highest score and last as long as you can! diff --git a/assets/texts/manual/tunernome.md b/assets/texts/manual/tunernome.md new file mode 100644 index 00000000..1f2885cf --- /dev/null +++ b/assets/texts/manual/tunernome.md @@ -0,0 +1,19 @@ +# \c235Music\C + +--- + +# \c540Tunernome: Tuner\C + +\lC\c505_A full chromatic tuner, right on your Swadge!_\C\L + +The first screen you'll see upon entering Tunernome is the instrument tuner. This can be used to tune a 6-string acoustic guitar, 4-string violin, 4-string ukulele, and 5-string banjo, all in their standard tunings. It can also tune to any of the 12 semitone notes on the chromatic scale individually, or display the most prominent note it can hear. + +The notes on the screen correspond to the strings of the guitar, starting from the lowest string at the bottom left and moving clockwise to the highest string. Their positions are matched to the positions of the LEDs, which will light up blue if the associated note is flat, red if the note is sharp, and white if the note is in tune. + +# \c540Tunernome: Metronome\C + +\lC\c505_"Rhythm is just a *click* away!" - osu!_\C\L + +Tunernome also helps you keep an accurate tempo and look awesome doing it! This will display the current tempo in BPM (beats per minute) along with the number of beats in a measure. + +Every LED flash indicates a single beat (quarter note), with each green LED flash indicating the first beat of each new measure. diff --git a/emu/src/emu_main.c b/emu/src/emu_main.c index 951cdd80..010b22d8 100644 --- a/emu/src/emu_main.c +++ b/emu/src/emu_main.c @@ -48,6 +48,7 @@ #include "mode_jukebox.h" #include "mode_diceroller.h" #include "mode_copypasta.h" +#include "mode_manual.h" //Make it so we don't need to include any other C files in our build. #define CNFG_IMPLEMENTATION @@ -93,6 +94,7 @@ swadgeMode * allModes[] = &modeDiceRoller, &modeJukebox, &modeCopyPasta, + &modeManual, }; //============================================================================== diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 63c18fea..208f5f41 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -7,6 +7,7 @@ idf_component_register( "display/bresenham.c" "display/cndraw.c" "display/display.c" + "display/font.c" "display/palette.c" "meleeMenu.c" "modes/fighter/aabb_utils.c" @@ -28,6 +29,7 @@ idf_component_register( "modes/mode_gamepad.c" "modes/mode_jukebox.c" "modes/mode_main_menu.c" + "modes/mode_manual.c" "modes/mode_nvs_manager.c" "modes/mode_slide_whistle.c" "modes/mode_test.c" @@ -61,6 +63,7 @@ idf_component_register( "swadge_esp32.c" "swadge_util.c" "utils/linked_list.c" + "utils/markdown_parser.c" "utils/text_entry.c" INCLUDE_DIRS "." diff --git a/main/display/display.c b/main/display/display.c index edab817f..bb5b0f50 100644 --- a/main/display/display.c +++ b/main/display/display.c @@ -18,6 +18,8 @@ //============================================================================== #define CLAMP(x,l,u) ((x) < l ? l : ((x) > u ? u : (x))) +#define MIN(x,y) ((x)<(y)?(x):(y)) +#define MAX(x,y) ((x)>(y)?(x):(y)) //============================================================================== // Constant data @@ -627,383 +629,3 @@ void drawWsgTile(display_t* disp, const wsg_t* wsg, int32_t xOff, int32_t yOff) } } } - -/** - * @brief Load a font from ROM to RAM. Fonts are bitmapped image files that have - * a single height, all ASCII characters, and a width for each character. - * PNGs placed in the assets folder before compilation will be automatically - * flashed to ROM - * - * @param name The name of the font to load - * @param font A handle to load the font to - * @return true if the font was loaded successfully - * false if the font failed to load and should not be used - */ -bool loadFont(const char* name, font_t* font) -{ - // Read font from file - uint8_t* buf = NULL; - size_t bufIdx = 0; - uint8_t chIdx = 0; - size_t sz; - if(!spiffsReadFile(name, &buf, &sz, true)) - { - ESP_LOGE("FONT", "Failed to read %s", name); - return false; - } - - // Read the data into a font struct - font->h = buf[bufIdx++]; - - // Read each char - while(bufIdx < sz) - { - // Get an easy refence to this character - font_ch_t* this = &font->chars[chIdx++]; - - // Read the width - this->w = buf[bufIdx++]; - - // Figure out what size the char is - int pixels = font->h * this->w; - int bytes = (pixels / 8) + ((pixels % 8 == 0) ? 0 : 1); - - // Allocate space for this char and copy it over - this->bitmap = (uint8_t*) malloc(sizeof(uint8_t) * bytes); - memcpy(this->bitmap, &buf[bufIdx], bytes); - bufIdx += bytes; - } - - // Zero out any unused chars - while (chIdx <= '~' - ' ' + 1) - { - font->chars[chIdx].bitmap = NULL; - font->chars[chIdx++].w = 0; - } - - // Free the SPIFFS data - free(buf); - - return true; -} - -/** - * @brief Free the memory allocated for a font - * - * @param font The font to free memory from - */ -void freeFont(font_t* font) -{ - // using uint8_t instead of char because a char will overflow to -128 after the last char is freed (\x7f) - for(uint8_t idx = 0; idx <= '~' - ' ' + 1; idx++) - { - if (font->chars[idx].bitmap != NULL) - { - free(font->chars[idx].bitmap); - } - } -} - - -/** - * @brief Draw a single character from a font to a display - * - * @param disp The display to draw a character to - * @param color The color of the character to draw - * @param h The height of the character to draw - * @param ch The character bitmap to draw (includes the width of the char) - * @param xOff The x offset to draw the char at - * @param yOff The y offset to draw the char at - */ -void drawChar(display_t* disp, paletteColor_t color, int h, const font_ch_t* ch, int16_t xOff, int16_t yOff) -{ - // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG esp-2021r2-patch3) - int bitIdx = 0; - const uint8_t* bitmap = ch->bitmap; - int wch = ch->w; - - // Get a pointer to the end of the bitmap - const uint8_t* endOfBitmap = &bitmap[((wch * h) + 7) >> 3] - 1; - - // Don't draw off the bottom of the screen. - if( yOff + h > disp->h ) - { - h = disp->h - yOff; - } - - // Check Y bounds - if(yOff < 0) - { - // Above the display, do wacky math with -yOff - bitIdx -= yOff * wch; - bitmap += bitIdx >> 3; - bitIdx &= 7; - h += yOff; - yOff = 0; - } - - paletteColor_t* pxOutput = disp->pxFb + yOff * disp->w; - - for (int y = 0; y < h; y++) - { - // Figure out where to draw - int truncate = 0; - - int startX = xOff; - if( xOff < 0 ) - { - // Track how many groups of pixels we are skipping over - // that weren't displayed on the left of the screen. - startX = 0; - bitIdx += -xOff; - bitmap += bitIdx >> 3; - bitIdx &= 7; - } - int endX = xOff + wch; - if( endX > disp->w ) - { - // Track how many groups of pixels we are skipping over, - // if the letter falls off the end of the screen. - truncate = endX - disp->w; - endX = disp->w; - } - - uint8_t thisByte = *bitmap; - for (int drawX = startX; drawX < endX; drawX++) - { - // Figure out where to draw - // Check X bounds - if(thisByte & (1 << bitIdx)) - { - // Draw the pixel - pxOutput[drawX] = color; - } - - // Iterate over the bit data - if( 8 == ++bitIdx ) - { - bitIdx = 0; - // Make sure not to read past the bitmap - if(bitmap < endOfBitmap) - { - thisByte = *(++bitmap); - } - else - { - // No more bitmap, so return - return; - } - } - } - - // Handle any remaining bits if we have ended off the end of the display. - bitIdx += truncate; - bitmap += bitIdx >> 3; - bitIdx &= 7; - pxOutput += disp->w; - } -} - - -/** - * @brief Draw text to a display with the given color and font - * - * @param disp The display to draw a character to - * @param font The font to use for the text - * @param color The color of the character to draw - * @param text The text to draw to the display - * @param xOff The x offset to draw the text at - * @param yOff The y offset to draw the text at - * @return The x offset at the end of the drawn string - */ -int16_t drawText(display_t* disp, const font_t* font, paletteColor_t color, const char* text, int16_t xOff, int16_t yOff) -{ - while(*text >= ' ') - { - // Only draw if the char is on the screen - if (xOff + font->chars[(*text) - ' '].w >= 0) - { - // Draw char - drawChar(disp, color, font->h, &font->chars[(*text) - ' '], xOff, yOff); - } - - // Move to the next char - xOff += (font->chars[(*text) - ' '].w + 1); - text++; - - // If this char is offscreen, finish drawing - if(xOff >= disp->w) - { - return xOff; - } - } - return xOff; -} - -/** - * @brief Return the pixel width of some text in a given font - * - * @param font The font to use - * @param text The text to measure - * @return The width of the text rendered in the font - */ -uint16_t textWidth(const font_t* font, const char* text) -{ - uint16_t width = 0; - while(*text != 0) - { - if((*text) >= ' ') - { - width += (font->chars[(*text) - ' '].w + 1); - } - text++; - } - // Delete trailing space - if(0 < width) - { - width--; - } - return width; -} - -static const char* drawTextWordWrapInner(display_t* disp, const font_t* font, paletteColor_t color, const char* text, - int16_t *xOff, int16_t *yOff, int16_t xMax, int16_t yMax) -{ - const char* textPtr = text; - int16_t textX = *xOff, textY = *yOff; - int nextSpace, nextDash, nextNl; - int nextBreak; - char buf[64]; - - // don't dereference that null pointer - if (text == NULL) - { - return NULL; - } - - // while there is text left to print, and the text would not exceed the Y-bounds... - while (*textPtr && (textY + font->h <= yMax)) - { - *yOff = textY; - - // skip leading spaces if we're at the start of the line - for (; textX == *xOff && *textPtr == ' '; textPtr++); - - // handle newlines - if (*textPtr == '\n') - { - textX = *xOff; - textY += font->h + 1; - textPtr++; - continue; - } - - // if strchr() returns NULL, this will be negative... - // otherwise, nextSpace will be the index of the next space of textPtr - nextSpace = strchr(textPtr, ' ') - textPtr; - nextDash = strchr(textPtr, '-') - textPtr; - nextNl = strchr(textPtr, '\n') - textPtr; - - // copy as much text as will fit into the buffer - // leaving room for a null-terminator in case the string is longer - strncpy(buf, textPtr, sizeof(buf) - 1); - - // ensure there is always a null-terminator even if - buf[sizeof(buf) - 1] = '\0'; - - // worst case, there are no breaks remaining - // I think this strlen call is necessary? - nextBreak = strlen(buf); - - if (nextSpace >= 0 && nextSpace < nextBreak) - { - nextBreak = nextSpace + 1; - } - - if (nextDash >= 0 && nextDash < nextBreak) - { - nextBreak = nextDash + 1; - } - - if (nextNl >= 0 && nextNl < nextBreak) - { - nextBreak = nextNl; - } - - // end the string at the break - buf[nextBreak] = '\0'; - - // The text is longer than an entire line, so we must shorten it - if (*xOff + textWidth(font, buf) > xMax) - { - // shorten the text until it fits - while (textX + textWidth(font, buf) > xMax && nextBreak > 0) - { - buf[--nextBreak] = '\0'; - } - } - - // The text is longer than will fit on the rest of the current line - // Or we shortened it down to nothing. Either way, move to next line. - // Also, go back to the start of the loop so we don't - // accidentally overrun the yMax - if (textX + textWidth(font, buf) > xMax || nextBreak == 0) - { - // The line won't fit - textY += font->h + 1; - textX = *xOff; - continue; - } - - // the line must have enough space for the rest of the buffer - // print the line, and advance the text pointer and offset - if (disp != NULL && textY + font->h >= 0 && textY <= disp->h) - { - textX = drawText(disp, font, color, buf, textX, textY); - } - else - { - // drawText returns the next text position, which is 1px past the last char - // textWidth returns, well, the text width, so add 1 to account for the last pixel - textX += textWidth(font, buf) + 1; - } - textPtr += nextBreak; - } - - // Return NULL if we've printed everything - // Otherwise, return the remaining text - *xOff = textX; - return *textPtr ? textPtr : NULL; -} - - -/// @brief Draws text, breaking on word boundaries, until the given bounds are filled or all text is drawn. -/// -/// Text will be drawn, starting at `(xOff, yOff)`, wrapping to the next line at ' ' or '-' when the next -/// word would exceed `xMax`, or immediately when a newline ('\\n') is encountered. Carriage returns and -/// tabs ('\\r', '\\t') are not supported. When the bottom of the next character would exceed `yMax`, no more -/// text is drawn and a pointer to the next undrawn character within `text` is returned. If all text has -/// been written, NULL is returned. -/// -/// @param disp The display on which to draw the text -/// @param font The font to use when drawing the text -/// @param color The color of the text to be drawn -/// @param text The text to be pointed, as a null-terminated string -/// @param xOff The X-coordinate to begin drawing the text at -/// @param yOff The Y-coordinate to begin drawing the text at -/// @param xMax The maximum x-coordinate at which any text may be drawn -/// @param yMax The maximum ycoordinate at which text may be drawn -/// @return A pointer to the first unprinted character within `text`, or NULL if all text has been written -const char* drawTextWordWrap(display_t* disp, const font_t* font, paletteColor_t color, const char* text, - int16_t *xOff, int16_t *yOff, int16_t xMax, int16_t yMax) -{ - return drawTextWordWrapInner(disp, font, color, text, xOff, yOff, xMax, yMax); -} - -uint16_t textHeight(const font_t* font, const char* text, int16_t width, int16_t maxHeight) -{ - int16_t xEnd = 0; - int16_t yEnd = 0; - drawTextWordWrapInner(NULL, font, cTransparent, text, &xEnd, &yEnd, width, maxHeight); - return yEnd + font->h + 1; -} diff --git a/main/display/display.h b/main/display/display.h index 029387c2..1dc1e299 100644 --- a/main/display/display.h +++ b/main/display/display.h @@ -85,18 +85,6 @@ struct display typedef struct display display_t; -typedef struct -{ - uint8_t w; - uint8_t* bitmap; -} font_ch_t; - -typedef struct -{ - uint8_t h; - font_ch_t chars['~' - ' ' + 2]; // enough space for all printed ascii chars, and pi -} font_t; - //============================================================================== // Prototypes //============================================================================== @@ -112,17 +100,6 @@ void drawWsgSimpleFast(display_t* disp, const wsg_t* wsg, int16_t xOff, int16_t void drawWsgTile(display_t* disp, const wsg_t* wsg, int32_t xOff, int32_t yOff); void freeWsg(wsg_t* wsg); -bool loadFont(const char* name, font_t* font); -void drawChar(display_t* disp, paletteColor_t color, int h, const font_ch_t* ch, - int16_t xOff, int16_t yOff); -int16_t drawText(display_t* disp, const font_t* font, paletteColor_t color, - const char* text, int16_t xOff, int16_t yOff); -const char* drawTextWordWrap(display_t* disp, const font_t* font, paletteColor_t color, const char* text, - int16_t *xOff, int16_t *yOff, int16_t xMax, int16_t yMax); -uint16_t textWidth(const font_t* font, const char* text); -uint16_t textHeight(const font_t* font, const char* text, int16_t width, int16_t maxHeight); -void freeFont(font_t* font); - // If you want to do your own thing. extern const int16_t sin1024[360]; diff --git a/main/display/font.c b/main/display/font.c new file mode 100644 index 00000000..600bd97d --- /dev/null +++ b/main/display/font.c @@ -0,0 +1,525 @@ +#include "font.h" + +#include +#include +#include +#include +#include +#include + +#include "display.h" +#include "palette.h" + +#include "../../components/hdw-spiffs/spiffs_manager.h" + +#define MIN(x,y) ((x)<(y)?(x):(y)) +#define MAX(x,y) ((x)>(y)?(x):(y)) +#define SLANT(font) ((font->h + 3) / 4) + +/** + * @brief Load a font from ROM to RAM. Fonts are bitmapped image files that have + * a single height, all ASCII characters, and a width for each character. + * PNGs placed in the assets folder before compilation will be automatically + * flashed to ROM + * + * @param name The name of the font to load + * @param font A handle to load the font to + * @return true if the font was loaded successfully + * false if the font failed to load and should not be used + */ +bool loadFont(const char* name, font_t* font) +{ + // Read font from file + uint8_t* buf = NULL; + size_t bufIdx = 0; + uint8_t chIdx = 0; + size_t sz; + if(!spiffsReadFile(name, &buf, &sz, true)) + { + ESP_LOGE("FONT", "Failed to read %s", name); + return false; + } + + // Read the data into a font struct + font->h = buf[bufIdx++]; + + // Read each char + while(bufIdx < sz) + { + // Get an easy refence to this character + font_ch_t* this = &font->chars[chIdx++]; + + // Read the width + this->w = buf[bufIdx++]; + + // Figure out what size the char is + int pixels = font->h * this->w; + int bytes = (pixels / 8) + ((pixels % 8 == 0) ? 0 : 1); + + // Allocate space for this char and copy it over + this->bitmap = (uint8_t*) malloc(sizeof(uint8_t) * bytes); + memcpy(this->bitmap, &buf[bufIdx], bytes); + bufIdx += bytes; + } + + // Zero out any unused chars + while (chIdx <= '~' - ' ' + 1) + { + font->chars[chIdx].bitmap = NULL; + font->chars[chIdx++].w = 0; + } + + // Free the SPIFFS data + free(buf); + + return true; +} + +/** + * @brief Free the memory allocated for a font + * + * @param font The font to free memory from + */ +void freeFont(font_t* font) +{ + // using uint8_t instead of char because a char will overflow to -128 after the last char is freed (\x7f) + for(uint8_t idx = 0; idx <= '~' - ' ' + 1; idx++) + { + if (font->chars[idx].bitmap != NULL) + { + free(font->chars[idx].bitmap); + } + } +} + + +/** + * @brief Draw a single character from a font to a display + * + * @param disp The display to draw a character to + * @param color The color of the character to draw + * @param h The height of the character to draw + * @param ch The character bitmap to draw (includes the width of the char) + * @param xOff The x offset to draw the char at + * @param yOff The y offset to draw the char at + * @param slant The slope of the slant, with 1 being 45 degrees and `h` or 0 being 90 degrees (normal) + */ +void drawCharItalic(display_t* disp, paletteColor_t color, int h, const font_ch_t* ch, int16_t xOff, int16_t yOff, int8_t slant) +{ + // This function has been micro optimized by cnlohr on 2022-09-07, using gcc version 8.4.0 (crosstool-NG esp-2021r2-patch3) + int bitIdx = 0; + const uint8_t* bitmap = ch->bitmap; + int wch = ch->w; + + // Get a pointer to the end of the bitmap + const uint8_t* endOfBitmap = &bitmap[((wch * h) + 7) >> 3] - 1; + int origH = h; + + // Don't draw off the bottom of the screen. + if( yOff + h > disp->h ) + { + h = disp->h - yOff; + } + + // Check Y bounds + if(yOff < 0) + { + // Above the display, do wacky math with -yOff + bitIdx -= yOff * wch; + bitmap += bitIdx >> 3; + bitIdx &= 7; + h += yOff; + origH += yOff; + yOff = 0; + } + + paletteColor_t* pxOutput = disp->pxFb + yOff * disp->w; + + for (int y = 0; y < h; y++) + { + // Figure out where to draw + int truncate = 0; + // for every (slant) lines we draw, we shift by one less + int xSlant = slant ? (origH - y) / slant : 0; + + int startX = xOff + xSlant; + if( xOff < 0 ) + { + // Track how many groups of pixels we are skipping over + // that weren't displayed on the left of the screen. + startX = 0; + bitIdx += -xOff; + bitmap += bitIdx >> 3; + bitIdx &= 7; + } + int endX = xOff + wch + xSlant; + if( endX > disp->w ) + { + // Track how many groups of pixels we are skipping over, + // if the letter falls off the end of the screen. + truncate = endX - disp->w; + endX = disp->w; + } + + uint8_t thisByte = *bitmap; + for (int drawX = startX; drawX < endX; drawX++) + { + // Figure out where to draw + // Check X bounds + if(thisByte & (1 << bitIdx)) + { + // Draw the pixel + pxOutput[drawX] = color; + } + + // Iterate over the bit data + if( 8 == ++bitIdx ) + { + bitIdx = 0; + // Make sure not to read past the bitmap + if(bitmap < endOfBitmap) + { + thisByte = *(++bitmap); + } + else + { + // No more bitmap, so return + return; + } + } + } + + // Handle any remaining bits if we have ended off the end of the display. + bitIdx += truncate; + bitmap += bitIdx >> 3; + bitIdx &= 7; + pxOutput += disp->w; + } +} + +void drawChar(display_t* disp, paletteColor_t color, int h, const font_ch_t* ch, int16_t xOff, int16_t yOff) +{ + drawCharItalic(disp, color, h, ch, xOff, yOff, 0); +} + +/** + * @brief Draw text to a display with the given color and font + * + * @param disp The display to draw a character to + * @param font The font to use for the text + * @param color The color of the character to draw + * @param text The text to draw to the display + * @param xOff The x offset to draw the text at + * @param yOff The y offset to draw the text at + * @param textAttrs A bitfield of textAttr_t indicating which options + * @return The x offset at the end of the drawn string + */ +int16_t drawTextAttrs(display_t* disp, const font_t* font, paletteColor_t color, const char* text, int16_t xOff, int16_t yOff, uint8_t textAttrs) +{ + // this is the extra space needed to account for the end of where the text will be drawn! + // this is not the extra space we need to add between each character, because that should + // not include any extra space for italics + uint8_t extraW = textWidthAttrs(font, "", textAttrs); + int16_t xStart = xOff; + + while(*text >= ' ') + { + // Only draw if the char is on the screen + if (xOff + font->chars[(*text) - ' '].w + extraW >= 0) + { + if (textAttrs & TEXT_BOLD) + { + for (uint8_t n = 0; n < 4; n++) + { + if (textAttrs & TEXT_ITALIC) + { + drawCharItalic(disp, color, font->h, &font->chars[(*text) - ' '], xOff + (n & 1), yOff + (n >> 1), SLANT(font)); + } + else + { + drawChar(disp, color, font->h, &font->chars[(*text) - ' '], xOff + (n & 1), yOff + (n >> 1)); + } + } + + // Account for the extra space + xOff++; + } + else if (textAttrs & TEXT_ITALIC) + { + // Draw char + drawCharItalic(disp, color, font->h, &font->chars[(*text) - ' '], xOff, yOff, SLANT(font)); + } + else + { + drawChar(disp, color, font->h, &font->chars[(*text) - ' '], xOff, yOff); + } + } + + // Move to the next char + xOff += (font->chars[(*text) - ' '].w + 1); + + // If this char is offscreen, finish drawing + if(xOff >= disp->w) + { + break; + } + + // Increment the text after we break so it's easier to check what the last char drawn was + text++; + } + + // TODO: Do we need to do something to make sure we don't draw the line farther than makes sense? + // Like, if we reached the end of the line (so *text != '\0') + + if (textAttrs & TEXT_STRIKE) + { + int16_t x1 = MAX(xStart, xOff); + SETUP_FOR_TURBO( disp ); + for (int16_t x = MIN(xStart, xOff); x < x1; x++) + { + TURBO_SET_PIXEL_BOUNDS(disp, x, yOff + font->h / 2, color); + } + } + + if (textAttrs & TEXT_UNDERLINE) + { + int16_t x1 = MAX(xStart, xOff); + SETUP_FOR_TURBO( disp ); + for (int16_t x = MIN(xStart, xOff); x < x1; x++) + { + TURBO_SET_PIXEL_BOUNDS(disp, x, yOff + font->h + 1, color); + } + } + + return xOff; +} + +int16_t drawText(display_t* disp, const font_t* font, paletteColor_t color, const char* text, int16_t xOff, int16_t yOff) +{ + return drawTextAttrs(disp, font, color, text, xOff, yOff, TEXT_NORMAL); +} + +/** + * @brief Return the pixel width of some text in a given font + * + * @param font The font to use + * @param text The text to measure + * @return The width of the text rendered in the font + */ +uint16_t textWidth(const font_t* font, const char* text) +{ + uint16_t width = 0; + while(*text != 0) + { + if((*text) >= ' ') + { + width += (font->chars[(*text) - ' '].w + 1); + } + text++; + } + // Delete trailing space + if(0 < width) + { + width--; + } + return width; +} + +uint16_t textWidthAttrs(const font_t* font, const char* text, uint8_t textAttrs) +{ + return textWidth(font, text) + ((textAttrs & TEXT_ITALIC) ? font->h / abs(SLANT(font)) : 0) + ((textAttrs & TEXT_BOLD) ? 1 : 0); +} + +uint16_t textWidthExtra(const font_t* font, const char* text, uint8_t textAttrs, const char* textEnd) +{ + uint16_t width = 0; + while(text != textEnd) + { + if((*text) >= ' ') + { + width += (font->chars[(*text) - ' '].w + 1); + } + text++; + } + // Delete trailing space + if(0 < width) + { + width--; + } + return width + ((textAttrs & TEXT_ITALIC) ? font->h / abs(SLANT(font)) : 0) + ((textAttrs & TEXT_BOLD) ? 1 : 0); +} + +static const char* drawTextWordWrapInner(display_t* disp, const font_t* font, paletteColor_t color, const char* text, + int16_t *xOff, int16_t *yOff, int16_t xMin, int16_t yMin, int16_t xMax, int16_t yMax, + uint8_t textAttrs, const char* textEnd) +{ + const char* textPtr = text; + int16_t textX = *xOff, textY = *yOff; + const char* breakPtr = NULL; + size_t bufStrlen; + char buf[64]; + + // don't dereference that null pointer + if (text == NULL) + { + return NULL; + } + + const int16_t lineHeight = textLineHeight(font, textAttrs); + + if (textEnd == NULL) + { + textEnd = text + strlen(text); + } + + // while there is text left to print, and the text would not exceed the Y-bounds... + // Subtract 1 from the line height to account for the space we don't care about + while (textPtr < textEnd && (textY + lineHeight - 1 <= yMax)) + { + *yOff = textY; + + // skip leading spaces if we're at the start of the line + for (; textX == xMin && *textPtr == ' ' && textPtr < textEnd; textPtr++); + + // handle newlines + if (textPtr < textEnd && *textPtr == '\n') + { + textX = xMin; + textY += lineHeight; + textPtr++; + continue; + } + + // copy as much text as will fit into the buffer + // leaving room for a null-terminator in case the string is longer + strncpy(buf, textPtr, sizeof(buf) - 1); + + // ensure there is always a null-terminator even if + buf[sizeof(buf) - 1] = '\0'; + + breakPtr = strpbrk(textPtr, " -\n"); + + // if strpbrk() returns NULL, we didn't find a char + // otherwise, breakPtr will point to the first breakable char in textPtr + bufStrlen = strlen(buf); + + if (breakPtr == NULL || breakPtr > textEnd) + { + breakPtr = textEnd; + } + + if (breakPtr - textPtr > bufStrlen) + { + breakPtr = textPtr + bufStrlen; + } + + switch (*breakPtr) + { + case ' ': + case '-': + breakPtr++; + break; + + case '\n': + default: + break; + } + + // end the string at the break + buf[(breakPtr - textPtr)] = '\0'; + + // The text is longer than an entire line, so we must shorten it + if (xMin + textWidthAttrs(font, buf, textAttrs) > xMax) + { + // shorten the text until it fits + while (textX + textWidthAttrs(font, buf, textAttrs) > xMax && breakPtr > textPtr) + { + buf[(--breakPtr - textPtr)] = '\0'; + } + } + + // The text is longer than will fit on the rest of the current line + // Or we shortened it down to nothing. Either way, move to next line. + // Also, go back to the start of the loop so we don't + // accidentally overrun the yMax + if (textX + textWidthAttrs(font, buf, textAttrs) > xMax || breakPtr == textPtr) + { + // The line won't fit + textY += lineHeight; + textX = xMin; + continue; + } + + // the line must have enough space for the rest of the buffer + // print the line, and advance the text pointer and offset + if (disp != NULL && textY + lineHeight - 1 >= 0 && textY <= disp->h) + { + textX = drawTextAttrs(disp, font, color, buf, textX, textY, textAttrs); + } + else + { + // drawText returns the next text position, which is 1px past the last char + // textWidth returns, well, the text width, so add 1 to account for the last pixel + + // We can't use textWidthAttrs() here! That will include the extra width from + // italic text, and put way too much space between words and characters. + // Instead, manually add another 1px if the text is bold + textX += textWidth(font, buf) + 1; + } + textPtr = breakPtr; + } + + // Return NULL if we've printed everything + // Otherwise, return the remaining text + *xOff = textX; + return (textPtr == textEnd) ? NULL : textPtr; +} + + +/// @brief Draws text, breaking on word boundaries, until the given bounds are filled or all text is drawn. +/// +/// Text will be drawn, starting at `(xOff, yOff)`, wrapping to the next line at ' ' or '-' when the next +/// word would exceed `xMax`, or immediately when a newline ('\\n') is encountered. Carriage returns and +/// tabs ('\\r', '\\t') are not supported. When the bottom of the next character would exceed `yMax`, no more +/// text is drawn and a pointer to the next undrawn character within `text` is returned. If all text has +/// been written, NULL is returned. +/// +/// @param disp The display on which to draw the text +/// @param font The font to use when drawing the text +/// @param color The color of the text to be drawn +/// @param text The text to be pointed, as a null-terminated string +/// @param xOff The X-coordinate to begin drawing the text at +/// @param yOff The Y-coordinate to begin drawing the text at +/// @param xMax The maximum x-coordinate at which any text may be drawn +/// @param yMax The maximum ycoordinate at which text may be drawn +/// @return A pointer to the first unprinted character within `text`, or NULL if all text has been written +const char* drawTextWordWrap(display_t* disp, const font_t* font, paletteColor_t color, const char* text, + int16_t *xOff, int16_t *yOff, int16_t xMax, int16_t yMax) +{ + return drawTextWordWrapInner(disp, font, color, text, xOff, yOff, *xOff, *yOff, xMax, yMax, TEXT_NORMAL, NULL); +} + +const char* drawTextWordWrapExtra(display_t* disp, const font_t* font, paletteColor_t color, const char* text, + int16_t *xOff, int16_t *yOff, int16_t xMin, int16_t yMin,int16_t xMax, int16_t yMax, + uint8_t textAttrs, const char* textEnd) +{ + return drawTextWordWrapInner(disp, font, color, text, xOff, yOff, xMin, yMin, xMax, yMax, textAttrs, textEnd); +} + +uint16_t textLineHeight(const font_t* font, uint8_t textAttrs) +{ + return font->h + ((textAttrs & TEXT_BOLD) ? 1 : 0) + ((textAttrs & TEXT_UNDERLINE) ? 2 : 0) + 1; +} + +uint16_t textHeightAttrs(const font_t* font, const char* text, int16_t startX, int16_t startY, int16_t width, int16_t maxHeight, uint8_t textAttrs) +{ + int16_t xEnd = startX; + int16_t yEnd = startY; + drawTextWordWrapInner(NULL, font, cTransparent, text, &xEnd, &yEnd, 0, 0, width, maxHeight, TEXT_NORMAL, NULL); + return yEnd + textLineHeight(font, textAttrs); +} + +uint16_t textHeight(const font_t* font, const char* text, int16_t width, int16_t maxHeight) +{ + return textHeightAttrs(font, text, 0, 0, width, maxHeight, TEXT_NORMAL); +} diff --git a/main/display/font.h b/main/display/font.h new file mode 100644 index 00000000..63655698 --- /dev/null +++ b/main/display/font.h @@ -0,0 +1,55 @@ +#ifndef _FONT_H +#define _FONT_H + +#include +#include + +#include "display.h" +#include "palette.h" + +typedef struct +{ + uint8_t w; + uint8_t* bitmap; +} font_ch_t; + +typedef struct +{ + uint8_t h; + font_ch_t chars['~' - ' ' + 2]; // enough space for all printed ascii chars, and pi +} font_t; + +typedef enum +{ + TEXT_NORMAL = 0, + TEXT_ITALIC = 1, + TEXT_BOLD = 2, + TEXT_UNDERLINE = 4, + TEXT_STRIKE = 8, +} textAttr_t; + +bool loadFont(const char* name, font_t* font); +void drawChar(display_t* disp, paletteColor_t color, int h, const font_ch_t* ch, + int16_t xOff, int16_t yOff); +void drawCharItalic(display_t* disp, paletteColor_t color, int h, const font_ch_t* ch, + int16_t xOff, int16_t yOff, int8_t slant); +int16_t drawText(display_t* disp, const font_t* font, paletteColor_t color, + const char* text, int16_t xOff, int16_t yOff); +int16_t drawTextAttrs(display_t* disp, const font_t* font, paletteColor_t color, + const char* text, int16_t xOff, int16_t yOff, uint8_t textAttrs); +const char* drawTextWordWrap(display_t* disp, const font_t* font, paletteColor_t color, const char* text, + int16_t *xOff, int16_t *yOff, int16_t xMax, int16_t yMax); +const char* drawTextWordWrapExtra(display_t* disp, const font_t* font, paletteColor_t color, const char* text, + int16_t *xOff, int16_t *yOff, int16_t xMin, int16_t yMin,int16_t xMax, int16_t yMax, + uint8_t textAttrs, const char* textEnd); +uint16_t textWidth(const font_t* font, const char* text); +uint16_t textLineHeight(const font_t* font, uint8_t textAttrs); +uint16_t textHeight(const font_t* font, const char* text, int16_t width, int16_t maxHeight); +uint16_t textHeightAttrs(const font_t* font, const char* text, int16_t startX, int16_t startY, int16_t width, int16_t maxHeight, uint8_t textAttrs); +uint16_t textWidthAttrs(const font_t* font, const char* text, uint8_t textAttrs); +uint16_t textWidthExtra(const font_t* font, const char* text, uint8_t textAttrs, const char* textEnd); +void freeFont(font_t* font); + + + +#endif diff --git a/main/meleeMenu.h b/main/meleeMenu.h index d25ed465..e3546adc 100644 --- a/main/meleeMenu.h +++ b/main/meleeMenu.h @@ -6,6 +6,7 @@ //============================================================================== #include "display.h" +#include "font.h" #include "btn.h" //============================================================================== diff --git a/main/modes/fighter/mode_fighter.h b/main/modes/fighter/mode_fighter.h index f01d2fa2..de3ddd9f 100644 --- a/main/modes/fighter/mode_fighter.h +++ b/main/modes/fighter/mode_fighter.h @@ -8,6 +8,7 @@ #include "aabb_utils.h" #include "musical_buzzer.h" #include "swadgeMode.h" +#include "font.h" //============================================================================== // Defines diff --git a/main/modes/jumper/jumper_menu.c b/main/modes/jumper/jumper_menu.c index 731de5eb..7d657863 100644 --- a/main/modes/jumper/jumper_menu.c +++ b/main/modes/jumper/jumper_menu.c @@ -7,6 +7,7 @@ #include "swadgeMode.h" #include "meleeMenu.h" +#include "font.h" #include "jumper_menu.h" #include "mode_jumper.h" diff --git a/main/modes/jumper/mode_jumper.c b/main/modes/jumper/mode_jumper.c index 72b265cf..6261c8de 100644 --- a/main/modes/jumper/mode_jumper.c +++ b/main/modes/jumper/mode_jumper.c @@ -17,6 +17,7 @@ #include "aabb_utils.h" #include "settingsManager.h" #include "swadge_util.h" +#include "font.h" #include "mode_jumper.h" #include "jumper_menu.h" diff --git a/main/modes/jumper/mode_jumper.h b/main/modes/jumper/mode_jumper.h index 20af4eaf..3e28dfb6 100644 --- a/main/modes/jumper/mode_jumper.h +++ b/main/modes/jumper/mode_jumper.h @@ -4,6 +4,7 @@ #include "swadgeMode.h" #include "aabb_utils.h" #include "musical_buzzer.h" +#include "font.h" extern const song_t jumpDeathTune; extern const song_t jumpGameOverTune; diff --git a/main/modes/mode_colorchord.c b/main/modes/mode_colorchord.c index 575d4ee6..f9eeef7c 100644 --- a/main/modes/mode_colorchord.c +++ b/main/modes/mode_colorchord.c @@ -13,6 +13,7 @@ #include "settingsManager.h" #include "bresenham.h" #include "swadge_util.h" +#include "font.h" // For colorchord #include "embeddedout.h" diff --git a/main/modes/mode_copypasta.c b/main/modes/mode_copypasta.c index 3c0e337c..dae934a3 100644 --- a/main/modes/mode_copypasta.c +++ b/main/modes/mode_copypasta.c @@ -13,6 +13,7 @@ #include "mode_main_menu.h" #include "musical_buzzer.h" #include "esp_random.h" +#include "font.h" //============================================================================== // Defines diff --git a/main/modes/mode_credits.c b/main/modes/mode_credits.c index d629ed9f..5809a4a1 100644 --- a/main/modes/mode_credits.c +++ b/main/modes/mode_credits.c @@ -10,6 +10,7 @@ #include "mode_credits.h" #include "mode_main_menu.h" #include "musical_buzzer.h" +#include "font.h" //============================================================================== // Defines diff --git a/main/modes/mode_dance.c b/main/modes/mode_dance.c index 78650a37..b64629ae 100644 --- a/main/modes/mode_dance.c +++ b/main/modes/mode_dance.c @@ -20,6 +20,7 @@ #include "mode_dance.h" #include "embeddedout.h" #include "bresenham.h" +#include "font.h" #include "swadge_util.h" #include "settingsManager.h" #include "linked_list.h" diff --git a/main/modes/mode_diceroller.c b/main/modes/mode_diceroller.c index 27bffe15..70df7aef 100644 --- a/main/modes/mode_diceroller.c +++ b/main/modes/mode_diceroller.c @@ -18,6 +18,7 @@ #include "nvs_manager.h" #include "bresenham.h" #include "display.h" +#include "font.h" #include "embeddednf.h" #include "embeddedout.h" #include "musical_buzzer.h" diff --git a/main/modes/mode_main_menu.c b/main/modes/mode_main_menu.c index 1ee4379f..72d111b7 100644 --- a/main/modes/mode_main_menu.c +++ b/main/modes/mode_main_menu.c @@ -35,6 +35,7 @@ #include "mode_tunernome.h" #include "nvs_manager.h" #include "picross_menu.h" +#include "mode_manual.h" // #include "picross_select.h" #if defined(EMU) #include "emu_main.h" @@ -127,6 +128,7 @@ const char mainMenuGames[] = "Games"; const char mainMenuTools[] = "Tools"; const char mainMenuMusic[] = "Music"; const char mainMenuSettings[] = "Settings"; +const char mainMenuManual[] = "Manual"; const char mainMenuSecret[] = "Secrets"; const char mainMenuBack[] = "Back"; const char mainMenuSoundBgmOn[] = "Music: On"; @@ -610,6 +612,7 @@ void mainMenuSetUpToolsMenu(bool resetPos) addRowToMeleeMenu(mainMenu->menu, modeDance.modeName); addRowToMeleeMenu(mainMenu->menu, modePaint.modeName); addRowToMeleeMenu(mainMenu->menu, modeDiceRoller.modeName); + addRowToMeleeMenu(mainMenu->menu, mainMenuManual); addRowToMeleeMenu(mainMenu->menu, mainMenuBack); // Set the position if(resetPos) @@ -659,6 +662,10 @@ void mainMenuToolsCb(const char* opt) { switchToSwadgeMode(&modeDiceRoller); } + else if (mainMenuManual == opt) + { + switchToSwadgeMode(&modeManual); + } else if(mainMenuBack == opt) { mainMenuSetUpTopMenu(false); diff --git a/main/modes/mode_manual.c b/main/modes/mode_manual.c new file mode 100644 index 00000000..a11bd874 --- /dev/null +++ b/main/modes/mode_manual.c @@ -0,0 +1,410 @@ +#include "mode_manual.h" + +#include +#include +#include + +#include "swadgeMode.h" +#include "mode_main_menu.h" +#include "spiffs_txt.h" +#include "bresenham.h" +#include "linked_list.h" + +#include "markdown_parser.h" + +#define MANUAL_FONT_UI "ibm_vga8.font" +#define MANUAL_FONT_HEADER "mm.font" +#define MANUAL_TOP_MARGIN 13 +#define MANUAL_BOTTOM_MARGIN 24 +#define MANUAL_SIDE_MARGIN 13 + +// #define WAITING_ICON + +void manualEnterMode(display_t* disp); +void manualExitMode(void); +void manualMainLoop(int64_t elapsedUs); +void manualButtonCb(buttonEvt_t* evt); + +void manualNextPage(void); +void manualPrevPage(void); + +typedef enum +{ + NONE, + NEXT_PAGE, + PREV_PAGE, + RELOAD_PAGE, +} nav_t; + +typedef struct +{ + char* file; + char* title; +} chapter_t; + +typedef struct +{ + const chapter_t* chapter; + markdownContinue_t* mdPos; +} page_t; + +const chapter_t chapters[] = +{ + {.file = "manual_intro.md", .title = "Introduction"}, + {.file = "main_menu.md", .title = "Main Menu"}, + {.file = "gamepad.md", .title = "Gamepad"}, + {.file = "light_dances.md", .title = "Light Dances"}, + {.file = "mfpaint.md", .title = "MFPaint"}, + {.file = "dice_roller.md", .title = "Dice Roller"}, + {.file = "tunernome.md", .title = "Tunernome"}, + {.file = "colorchord.md", .title = "Colorchord"}, + {.file = "techno_slide_whistle.md", .title = "TechnoSlideWhistle"}, + {.file = "jukebox.md", .title = "Jukebox"}, + {.file = "swadge_bros.md", .title = "Swadge Bros"}, + {.file = "super_swadge_land.md", .title = "Super Swadge Land"}, + {.file = "donut_jump.md", .title = "Donut Jump"}, + {.file = "pi_cross.md", .title = "Pi-Cross"}, + {.file = "flyin_donut.md", .title = "Flyin' Donut"}, + {.file = "tiltrads_color.md", .title = "Tiltrads Color"}, +}; + +const chapter_t* lastChapter = chapters + sizeof(chapters) / sizeof(chapters[0]) - 1; + +swadgeMode modeManual = +{ + .modeName = "Manual", + .fnEnterMode = manualEnterMode, + .fnExitMode = manualExitMode, + .fnMainLoop = manualMainLoop, + .fnButtonCallback = manualButtonCb, + .fnBackgroundDrawCallback = NULL, + .fnTouchCallback = NULL, + .wifiMode = NO_WIFI, + .fnEspNowRecvCb = NULL, + .fnEspNowSendCb = NULL, + .fnAccelerometerCallback = NULL, + .fnAudioCallback = NULL, + .fnTemperatureCallback = NULL, + .overrideUsb = false +}; + +typedef struct +{ + display_t* disp; + font_t uiFont, bodyFont, headerFont; + wsg_t arrowWsg; + + paletteColor_t bgColor, fgColor; + + markdownText_t* markdown; + markdownContinue_t* mdPosition; + markdownParams_t mdParams; + + const chapter_t* chapter; + + char* text; + list_t pages; + node_t* curPage; + + nav_t navigate; + bool reload; +} manual_t; + +manual_t* manual; + +node_t* paginateText(markdownText_t* markdown, list_t* container) +{ + node_t* firstPage = NULL; + page_t* newPage = NULL; + markdownContinue_t* nextPos = NULL; + + do + { + newPage = malloc(sizeof(page_t)); + newPage->chapter = manual->chapter; + + // copyContinue will return a NULL if passed a NULL + newPage->mdPos = copyContinue(nextPos); + push(&manual->pages, newPage); + + if (firstPage == NULL) + { + firstPage = manual->pages.last; + } + } + while (drawMarkdown(NULL, markdown, &manual->mdParams, &nextPos, true)); + + if (nextPos != NULL) + { + free(nextPos); + } + + return firstPage; +} + +void manualLoadText(bool reverse) +{ + ESP_LOGD("Manual", "Loading text"); + if (manual->markdown != NULL) + { + freeMarkdown(manual->markdown); + manual->markdown = NULL; + } + + if (manual->text != NULL) + { + free(manual->text); + manual->text = NULL; + } + + manual->text = loadTxt(manual->chapter->file); + if (manual->text == NULL) + { + manual->text = "# Could not load text :("; + } + + manual->markdown = parseMarkdown(manual->text); + + // if the page we're trying to use hasn't been created yet, we need to create it + if (manual->curPage == NULL)// || ((page_t*)manual->curPage->val)->chapter != manual->chapter) + { + manual->curPage = paginateText(manual->markdown, &manual->pages); + + if (reverse) + { + manual->curPage = manual->pages.last; + } + } +} + +void manualEnterMode(display_t* disp) +{ + manual = calloc(1, sizeof(manual_t)); + + manual->disp = disp; + + manual->fgColor = c555; + manual->bgColor = c012; + + manual->navigate = RELOAD_PAGE; + manual->reload = true; + + loadFont(MANUAL_FONT_UI, &manual->uiFont); + manual->bodyFont = manual->uiFont; + loadFont(MANUAL_FONT_HEADER, &manual->headerFont); + + loadWsg("arrow12.wsg", &manual->arrowWsg); + + markdownParams_t params = + { + .xMin = MANUAL_SIDE_MARGIN, .yMin = MANUAL_TOP_MARGIN, + .xMax = manual->disp->w - MANUAL_SIDE_MARGIN, .yMax = manual->disp->h - MANUAL_BOTTOM_MARGIN, + .bodyFont = &manual->bodyFont, + .headerFont = &manual->headerFont, + .color = manual->fgColor, + }; + + memcpy(&manual->mdParams, ¶ms, sizeof(markdownParams_t)); + + manual->chapter = chapters; +} + +void manualExitMode(void) +{ + freeFont(&manual->bodyFont); + freeFont(&manual->headerFont); + + freeWsg(&manual->arrowWsg); + + page_t* page; + while (NULL != (page = pop(&manual->pages))) + { + free(page->mdPos); + free(page); + } + // maybe redundant but oh well + clear(&manual->pages); + + if (manual->markdown != NULL) + { + freeMarkdown(manual->markdown); + } + + if (manual->text != NULL) + { + free(manual->text); + } + free(manual); +} + +void manualMainLoop(int64_t elapsedUs) +{ +#define hasNext (manual->curPage->next != NULL || manual->chapter < lastChapter) +#define hasPrev (manual->curPage->prev != NULL) + + if (manual->navigate != NONE) + { + if (manual->reload) + { + if (manual->navigate == NEXT_PAGE) + { + manualNextPage(); + } + else if (manual->navigate == PREV_PAGE) + { + manualPrevPage(); + } + else if (manual->navigate == RELOAD_PAGE) + { + manualLoadText(false); + } + + // Clear the screen + fillDisplayArea(manual->disp, 0, 0, manual->disp->w, manual->disp->h, manual->bgColor); + + page_t* curPage = manual->curPage->val; + drawMarkdown(manual->disp, manual->markdown, &manual->mdParams, &curPage->mdPos, false); + + manual->navigate = NONE; + manual->reload = false; + } + else + { + if (manual->navigate == RELOAD_PAGE + || (manual->navigate == NEXT_PAGE && hasNext) + || (manual->navigate == PREV_PAGE && hasPrev)) + { + // Just draw the waiting icon and set us up to reload on the next frame + manual->reload = true; + +#ifdef WAITING_ICON + #define HOURGLASS_W 20 + #define HOURGLASS_H 37 + + plotCircleFilled(manual->disp, manual->disp->w / 2, manual->disp->h / 2, HOURGLASS_H / 2 + 6, manual->bgColor); + plotCircle(manual->disp, manual->disp->w / 2, manual->disp->h / 2, HOURGLASS_H / 2 + 6, manual->fgColor); + int16_t x0, x1; + int16_t y0, y1; + + x0 = (manual->disp->w - HOURGLASS_W) / 2; + x1 = x0 + HOURGLASS_W; + y0 = (manual->disp->h - HOURGLASS_H) / 2; + y1 = y0 + HOURGLASS_H; + + // Top-Left -> Bottom Right + plotLine(manual->disp, x0, y0, x1, y1, manual->fgColor, 0); + // Top-Left -> Top Right + plotLine(manual->disp, x0, y0, x1, y0, manual->fgColor, 0); + // Top-Right -> Bottom Left + plotLine(manual->disp, x1, y0, x0, y1, manual->fgColor, 0); + // Bottom-Left -> Bottom-Right + plotLine(manual->disp, x0, y1, x1, y1, manual->fgColor, 0); +#endif + } + else + { + manual->navigate = NONE; + } + } + } + + fillDisplayArea(manual->disp, 0, 0, manual->disp->w, MANUAL_TOP_MARGIN, manual->bgColor); + fillDisplayArea(manual->disp, 0, 0, MANUAL_SIDE_MARGIN, manual->disp->h, manual->bgColor); + fillDisplayArea(manual->disp, manual->disp->w - MANUAL_SIDE_MARGIN + 1, 0, manual->disp->w, manual->disp->h, manual->bgColor); + fillDisplayArea(manual->disp, 0, manual->disp->h - MANUAL_BOTTOM_MARGIN + 1, manual->disp->w, manual->disp->h, manual->bgColor); + + plotLine(manual->disp, 0, manual->disp->h - MANUAL_BOTTOM_MARGIN + 1, manual->disp->w, manual->disp->h - MANUAL_BOTTOM_MARGIN + 1, manual->fgColor, 0); + + // || manual->curPage->next != NULL || curPage->chapter < lastChapter + if (hasNext) + { + // Draw a "next page" arrow + drawWsg(manual->disp, &manual->arrowWsg, manual->disp->w - 40 - manual->arrowWsg.w, manual->disp->h - (MANUAL_BOTTOM_MARGIN - manual->arrowWsg.h) / 2 - manual->arrowWsg.h, false, false, 90); + } + + if (hasPrev) + { + // Draw a "prev page" arrow + drawWsg(manual->disp, &manual->arrowWsg, 40, manual->disp->h - (MANUAL_BOTTOM_MARGIN - manual->arrowWsg.h) / 2 - manual->arrowWsg.h, false, false, 270); + } + + +} + +void manualNextChapter(void) +{ + if (manual->chapter != lastChapter) + { + manual->chapter++; + manual->curPage = manual->curPage->next; + manualLoadText(false); + } +} + +void manualPrevChapter(void) +{ + if (manual->chapter > chapters) + { + manual->chapter--; + manual->curPage = manual->curPage->prev; + manualLoadText(true); + } +} + +void manualNextPage(void) +{ + // if there is a next page, and it is in the same chapter, go to it + // if there is not a next page, or it is in a different chapter, go to next chapter + if (manual->curPage->next != NULL && ((page_t*)manual->curPage->next->val)->chapter == manual->chapter) + { + manual->curPage = manual->curPage->next; + } + else + { + manualNextChapter(); + } +} + +void manualPrevPage(void) +{ + if (manual->curPage->prev != NULL && ((page_t*)manual->curPage->prev->val)->chapter == manual->chapter) + { + manual->curPage = manual->curPage->prev; + } + else + { + manualPrevChapter(); + } +} + +void manualButtonCb(buttonEvt_t* evt) +{ + if (evt->down) + { + switch (evt->button) + { + case UP: + case DOWN: + break; + + case LEFT: + manual->navigate = PREV_PAGE; + break; + + case RIGHT: + manual->navigate = NEXT_PAGE; + break; + + case BTN_A: + break; + + case START: + case BTN_B: + switchToSwadgeMode(&modeMainMenu); + break; + + case SELECT: + break; + } + } +} \ No newline at end of file diff --git a/main/modes/mode_manual.h b/main/modes/mode_manual.h new file mode 100644 index 00000000..2ef13055 --- /dev/null +++ b/main/modes/mode_manual.h @@ -0,0 +1,8 @@ +#ifndef _MODE_MANUAL_H_ +#define _MODE_MANUAL_H_ + +#include "swadgeMode.h" + +extern swadgeMode modeManual; + +#endif diff --git a/main/modes/mode_slide_whistle.c b/main/modes/mode_slide_whistle.c index 318cf159..66a4f6ee 100644 --- a/main/modes/mode_slide_whistle.c +++ b/main/modes/mode_slide_whistle.c @@ -18,6 +18,7 @@ #include "bresenham.h" #include "display.h" +#include "font.h" #include "embeddednf.h" #include "embeddedout.h" #include "esp_timer.h" diff --git a/main/modes/mode_test.c b/main/modes/mode_test.c index c0b975b9..5b86e580 100644 --- a/main/modes/mode_test.c +++ b/main/modes/mode_test.c @@ -10,6 +10,7 @@ #include "settingsManager.h" #include "embeddedout.h" #include "bresenham.h" +#include "font.h" #include "musical_buzzer.h" #include "led_util.h" #include "swadge_util.h" diff --git a/main/modes/mode_tiltrads.c b/main/modes/mode_tiltrads.c index 10c5849f..f6c5ed43 100644 --- a/main/modes/mode_tiltrads.c +++ b/main/modes/mode_tiltrads.c @@ -16,7 +16,8 @@ #include "mode_tiltrads.h" #include "esp_timer.h" // Timer functions #include "esp_log.h" // Debug logging functions -#include "display.h" // Display functions and draw text +#include "display.h" // Display functions +#include "font.h" // Draw text #include "bresenham.h" // Draw shapes #include "linked_list.h" // Custom linked list #include "nvs_manager.h" // Saving and loading high scores and last scores diff --git a/main/modes/mode_tunernome.c b/main/modes/mode_tunernome.c index dc8229d8..b145a444 100644 --- a/main/modes/mode_tunernome.c +++ b/main/modes/mode_tunernome.c @@ -18,6 +18,7 @@ #include "bresenham.h" #include "display.h" +#include "font.h" #include "embeddednf.h" #include "embeddedout.h" #include "esp_timer.h" diff --git a/main/modes/picross/picross_select.c b/main/modes/picross/picross_select.c index d3c38023..38d66532 100644 --- a/main/modes/picross/picross_select.c +++ b/main/modes/picross/picross_select.c @@ -15,6 +15,7 @@ #include "picross_select.h" #include "picross_menu.h" #include "bresenham.h" +#include "font.h" //==== // globals, basically diff --git a/main/modes/picross/picross_select.h b/main/modes/picross/picross_select.h index 70ac53b9..ca177198 100644 --- a/main/modes/picross/picross_select.h +++ b/main/modes/picross/picross_select.h @@ -4,6 +4,7 @@ #include "swadgeMode.h" #include "aabb_utils.h" #include "picross_consts.h" +#include "font.h" typedef struct { int8_t index; diff --git a/main/modes/platformer/mode_platformer.c b/main/modes/platformer/mode_platformer.c index c82bbc37..98e9cc40 100644 --- a/main/modes/platformer/mode_platformer.c +++ b/main/modes/platformer/mode_platformer.c @@ -22,6 +22,7 @@ #include "entityManager.h" #include "leveldef.h" #include "led_util.h" +#include "font.h" #include "palette.h" #include "nvs_manager.h" #include "platformer_sounds.h" diff --git a/main/utils/linked_list.c b/main/utils/linked_list.c index 7e00f425..94996cfa 100644 --- a/main/utils/linked_list.c +++ b/main/utils/linked_list.c @@ -15,6 +15,8 @@ #include "linked_list.h" +//#define DEBUG + /* Uncomment just one of these */ // #define VALIDATE_LIST(func, line, nl, list, target) validateList(func, line, nl, list, target) #define VALIDATE_LIST(func, line, nl, list, target) @@ -32,11 +34,13 @@ void validateList(const char * func, int line, bool nl, list_t * list, node_t * */ void validateList(const char * func, int line, bool nl, list_t * list, node_t * target) { +#ifdef DEBUG if(nl) { printf("\n"); } ESP_LOGD("VL", "%s::%d, len: %d (%p)", func, line, list->length, target); +#endif node_t* currentNode = list->first; node_t* prev = NULL; int countedLen = 0; @@ -55,7 +59,9 @@ void validateList(const char * func, int line, bool nl, list_t * list, node_t * while (currentNode != NULL) { +#ifdef DEBUG ESP_LOGD("VL", "%p -> %p -> %p", currentNode->prev, currentNode, currentNode->next); +#endif if(prev != currentNode->prev) { ESP_LOGE("VL", "Linkage error %p != %p", currentNode->prev, prev); diff --git a/main/utils/markdown_parser.c b/main/utils/markdown_parser.c new file mode 100644 index 00000000..e68eb764 --- /dev/null +++ b/main/utils/markdown_parser.c @@ -0,0 +1,1937 @@ +#include "markdown_parser.h" + +#include +#include +#include +#include +#include +#include + +#include "palette.h" +#include "display.h" +#include "font.h" +#include "bresenham.h" + +//#define DEBUG + +#ifdef DEBUG +#define MDLOG(...) printf(__VA_ARGS__) +#else +#define MDLOG(...) +#endif + +//ESP_LOGD("Markdown", __VA_ARGS__) +#define MIN(x,y) ((x)<(y)?(x):(y)) +#define MAX(x,y) ((x)>(y)?(x):(y)) + +#define MD_MALLOC(size) heap_caps_malloc(size, MALLOC_CAP_SPIRAM) +//#define MD_MALLOC(size) malloc(size) + +#define NO_BACKTRACK UINT32_MAX + +typedef enum +{ + ADD_SIBLING = 1, + ADD_CHILD = 2, + GO_TO_PARENT = 4, +} nodeAction_t; + +typedef struct +{ + const char* start; + const char* end; +} mdText_t; + +typedef enum +{ + STYLE, + ALIGN, + BREAK, + COLOR, + FONT, +} mdOptType_t; + +typedef struct +{ + mdOptType_t type; + union { + textStyle_t style; + textAlign_t align; + textBreak_t breakMode; + paletteColor_t color; + mdText_t font; + }; +} mdOpt_t; + +typedef enum +{ + HORIZONTAL_RULE, + WORD_BREAK, + LINE_BREAK, + PARAGRAPH_BREAK, + PAGE_BREAK, + BULLET, + HEADER, +} mdDec_t; + +typedef enum +{ + TEXT, + OPTION, + DECORATION, + IMAGE, + CUSTOM, + EMPTY, +} mdNodeType_t; + +typedef struct mdNode +{ + struct mdNode* parent; + struct mdNode* child; + struct mdNode* next; + uint32_t index; + + mdNodeType_t type; + + union { + mdText_t text; + mdOpt_t option; + mdDec_t decoration; + mdText_t image; + void* custom; + }; +} mdNode_t; + +typedef struct +{ + const char* text; + mdNode_t* tree; +} _markdownText_t; + +typedef struct _markdownContinue_t +{ + size_t textPos; + uint32_t treeIndex; +} _markdownContinue_t; + +typedef struct +{ + // The node to print + // This may either be TEXT, DECORATION.WORD_BREAK, or IMAGE (eventually) + const mdNode_t* node; + + // we can technically figure out the style from the tree, but we have plenty of SPIRAM and not a lot of speed + textStyle_t textStyle; + paletteColor_t color; + const font_t* font; +} mdLinePartInfo_t; + +typedef struct +{ + // The total width of all parts + int16_t totalWidth; + + // In case the first node doesn't start at the beginning of the line, + // this is the starting offset for that text + size_t firstTextOffset; + + // The alignment of the first node of the line + // If this changes while we're printing the line, oh well! + // Maybe in the future, we can support + textAlign_t align; + + // The maximum height of any text within this line + int16_t lineHeight; + + mdLinePartInfo_t* parts; + size_t partCount; + size_t partAlloc; + + bool pending; +} mdLinePlan_t; + +typedef struct +{ + markdownParams_t defaults; + markdownParams_t params; + mdLinePlan_t linePlan; + + int16_t x, y; + const font_t* font; + + size_t textPos; + uint32_t backtrackIndex; + + // for storing pointers to arbitrary data + // in case we need to load a new font or something + void* data[16]; + uint8_t next; +} mdPrintState_t; + +static mdOpt_t headerOpts = { + .type = ALIGN, + .align = ALIGN_CENTER, +}; + + +static void parseMarkdownInner(const char* text, _markdownText_t* out); + +/// @brief Returns a pointer to the next node, allocating more space if necessary, and sets up the pointers +/// If `parent` is not NULL, it will be set as the returned node's `parent` pointer. +/// And, if `parent->child` is NULL, `parent->child` will be set to the returned node. +/// If `prev` is not NULL, `prev->next` will be set to the returned node. +/// @param data The _markdownText_t containing the tree root and the children backing array +/// @param parent The node to set as the parent of the new node +/// @param prev The node's previous sibling, if this is not the first node +/// @return +static mdNode_t* newNode(_markdownText_t* data, mdNode_t* parent, mdNode_t* prev); +static void freeTree(mdNode_t*); +static bool drawMarkdownNode(display_t* disp, const mdNode_t* node, const mdNode_t* prev, mdPrintState_t* state); + +static mdNode_t* newNode(_markdownText_t* data, mdNode_t* parent, mdNode_t* prev) +{ + if (parent == NULL && prev == NULL) + { + ESP_LOGE("Markdown", "Allocating root node"); + } + + mdNode_t* result = MD_MALLOC(sizeof(mdNode_t)); + result->type = EMPTY; + result->child = NULL; + result->next = NULL; + result->parent = NULL; + + if (parent != NULL) + { + result->parent = parent; + + if (parent->child == NULL) + { + parent->child = result; + } + } + + if (prev != NULL) + { + prev->next = result; + } + + return result; +} + +static const char* strndebug(const char* start, const char* end) { +#ifdef DEBUG + static char buffer[64]; + + if (start == NULL || end == NULL || end < start) + { + sprintf(buffer, "start=0x%08x, end=0x%08x", (int)start, (int)end); + return buffer; + } + + // if the start is: "hi there\0" + // and end is: "here\0" (so the actual string should be "hi t", strlen=4) + // then end - start is going to be 4, which is correct + size_t len = (end - start); + + if (len >= sizeof(buffer)) { + len = (sizeof(buffer) - 5) / 2; + strncpy(buffer, start, len); + buffer[len] = '.'; + buffer[len+1] = '.'; + buffer[len+2] = '.'; + strncpy(buffer + len + 3, end - (sizeof(buffer) - len - 3 - 1), sizeof(buffer) - len - 3); + } + else + { + strncpy(buffer, start, sizeof(buffer)); + buffer[end - start] = '\0'; + } + buffer[sizeof(buffer) - 1] = '\0'; + return buffer; +#else + return ""; +#endif +} + +#define PRINT_INDENT(...) for(uint8_t i = 0; i < indent; i++) printf(" "); printf(__VA_ARGS__) + +static void _printNode(const mdNode_t* node, int indent, bool detailed) +{ +#ifdef DEBUG + if (node == NULL) + { + PRINT_INDENT("NULL\n"); + } + else + { + switch(node->type) + { + case TEXT: + { + PRINT_INDENT("Text (0x%08x): %s\n", (int)node, strndebug(node->text.start, node->text.end)); + break; + } + + case OPTION: + { + switch (node->option.type) + { + case STYLE: + { + if (node->option.style == STYLE_NORMAL) + { + PRINT_INDENT("Style: Normal (0x%08x)\n", (int)node); + } + else + { + PRINT_INDENT("Style: "); + if (node->option.style & STYLE_ITALIC) + { + printf("Italic "); + } + + if (node->option.style & STYLE_UNDERLINE) + { + printf("Underline "); + } + + if (node->option.style & STYLE_STRIKE) + { + printf("Strikethrough "); + } + + if (node->option.style & STYLE_BOLD) + { + printf("Bold "); + } + + + printf("(0x%08x)\n", (int)node); + } + break; + } + + case ALIGN: + { + PRINT_INDENT("Align: "); + + if ((node->option.align & VALIGN_CENTER) == VALIGN_CENTER) + { + printf("Middle "); + } + else if (node->option.align & VALIGN_TOP) + { + printf("Top "); + } + else if (node->option.align & VALIGN_BOTTOM) + { + printf("Right "); + } + + + if ((node->option.align & ALIGN_CENTER) == ALIGN_CENTER) + { + printf("Center "); + } + else if (node->option.align & ALIGN_LEFT) + { + printf("Left "); + } + else if (node->option.align & ALIGN_RIGHT) + { + printf("Right "); + } + + printf("(0x%08x)\n", (int)node); + break; + } + + case BREAK: + { + PRINT_INDENT("Break... (0x%08x)\n", (int)node); + break; + } + + case COLOR: + { + PRINT_INDENT("Color: c%d%d%d (%d) (0x%08x)\n", node->option.color / 36, node->option.color / 6 % 6, node->option.color % 6, node->option.color, (int)node); + break; + } + + case FONT: + { + PRINT_INDENT("Font: '%s' (0x%08x)\n", strndebug(node->option.font.start, node->option.font.end), (int)node); + break; + } + + default: + { + PRINT_INDENT("???? (0x%08x)\n", (int)node); + break; + } + } + break; + } + + case DECORATION: + { + switch (node->decoration) + { + case WORD_BREAK: + PRINT_INDENT("Word Break (0x%08x)\n", (int)node); + break; + + case LINE_BREAK: + PRINT_INDENT("Line Break (0x%08x)\n", (int)node); + break; + + case PARAGRAPH_BREAK: + PRINT_INDENT("Paragraph Break (0x%08x)\n", (int)node); + break; + + case PAGE_BREAK: + PRINT_INDENT("Page break (0x%08x)\n", (int)node); + break; + + case HORIZONTAL_RULE: + PRINT_INDENT("Horizontal rule (0x%08x)\n", (int)node); + break; + + case BULLET: + PRINT_INDENT("Bullet (0x%08x)\n", (int)node); + break; + + case HEADER: + PRINT_INDENT("Header (0x%08x)\n", (int)node); + break; + + default: + PRINT_INDENT("Something else (0x%08x)\n", (int)node); + break; + } + break; + } + + case IMAGE: + { + PRINT_INDENT("Image: '%s' (0x%08x)\n", strndebug(node->image.start, node->image.end), (int)node); + break; + } + + case CUSTOM: + { + PRINT_INDENT("Custom... (0x%08x)\n", (int)node); + break; + } + + case EMPTY: + { + PRINT_INDENT("EMPTY (0x%08x)\n", (int)node); + break; + } + + default: + { + PRINT_INDENT("!!! SOMETHING ELSE (%d) !!! (0x%08x)\n", node->type, (int)node); + break; + } + } + + if (detailed) + { + PRINT_INDENT(" Parent: 0x%08x\n", (int)node->parent); + PRINT_INDENT(" Child: 0x%08x\n", (int)node->child); + PRINT_INDENT(" Next: 0x%08x\n", (int)node->next); + } + } +#endif +} + +static void printNode(const mdNode_t* node, int indent) +{ + _printNode(node, indent, false); +} + +static void printNodeDetailed(const mdNode_t* node, int indent) +{ + _printNode(node, indent, true); +} + +// after curNode is set up, checks if you can just merge it with lastNode instead +static bool mergeTextNodes(mdNode_t** curNode, mdNode_t** lastNode) +{ +#ifdef DEBUG + char buf1[64]; + char buf2[64]; + char buf3[64]; +#endif + if ((*curNode)->type == TEXT && *lastNode != NULL && (*lastNode)->type == TEXT && (*lastNode)->text.end == (*curNode)->text.start) + { +#ifdef DEBUG + strncpy(buf1, strndebug((*lastNode)->text.start, (*lastNode)->text.end), sizeof(buf1)-1); + strncpy(buf2, strndebug((*curNode)->text.start, (*curNode)->text.end), sizeof(buf2)-1); + strncpy(buf3, strndebug((*lastNode)->text.start, (*curNode)->text.end), sizeof(buf3)-1); + buf1[63] = '\0'; + buf2[63] = '\0'; + buf3[63] = '\0'; + + MDLOG("Merging text nodes '%s' and '%s' into '%s'\n", buf1, buf2, buf3); +#endif + // these nodes are contiguous, so just add onto them + (*lastNode)->text.end = (*curNode)->text.end; + + // mark the current node as empty + (*curNode)->type = EMPTY; + return true; + } + + return false; +} + +static void parseMarkdownInner(const char* text, _markdownText_t* out) +{ + int index = 0; + mdNode_t* curNode = out->tree = newNode(out, NULL, NULL); + curNode->index = index++; + mdNode_t* lastNode = NULL; + + #define START_OF_LINE() (lastNode == NULL || (lastNode->type == DECORATION && lastNode->decoration != BULLET && lastNode->decoration != HEADER && (lastNode->decoration != WORD_BREAK || lastNode->parent == NULL))) + #define PARENT_IS_SAME_OPTION(valtype) (curNode->parent != NULL && curNode->parent->type == curNode->type && curNode->parent->option.type == curNode->option.type && curNode->parent->option.valtype == curNode->option.valtype) + + // Temporary pointer to the text at the start of the loop so we can backtrack a tiny bit if needed + const char* textStart; + nodeAction_t action; + + while (*text) + { + // Most actions will just add a sibling, so do that by default + action = ADD_SIBLING; + + MDLOG("Parsing at %c (\\x%02x)\n", *text, *text); + textStart = text; + + switch (*text) + { + case '\\': + { + // is this a simple escape or a control sequence? + // escapable chars: \ _ * ~ # - + // reserved escape chars: abefnrtuUvx0123456789 + // escape sequence chars: + // c: change color, e.g. "\c000", "\c300", "\c555" + // C: revert color + // l: alignment, e.g. "\lR" or "\lC", options are L, R, C (left, right, center) and T, B, M (top, bottom, middle (v-center)) + // L: revert alignment + // d: font, e.g. "\d(mm.font)" or "\d(tom_thumb.font)" + // D: revert font + switch (*(++text)) + { + case '\\': + case '_': + case '*': + case '~': + case '#': + case '-': + case '!': + { + // regular escape + // create a text node for the single character after the escape + MDLOG("Escaping regular character %c (\\x%02x)\n", *text, *text); + curNode->type = TEXT; + curNode->text.start = text; + curNode->text.end = ++text; + break; + } + + case 'f': + { + curNode->type = DECORATION; + curNode->decoration = PAGE_BREAK; + ++text; + + break; + } + + case 'c': + { + bool colorOk = true; + int colorVal = 0; + for (uint8_t i = 0; i < 3; i++) + { + ++text; + if (*text >= '0' && *text <= '5') + { + colorVal *= 6; + colorVal += ((*text) - '0'); + } + else + { + colorOk = false; + } + } + + if (colorOk) + { + curNode->type = OPTION; + curNode->option.type = COLOR; + curNode->option.color = (paletteColor_t)colorVal; + ++text; + + // continue with the next node as a child + action = ADD_CHILD; + } + else + { + // format doesn't match so just mark the backslash as text + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = textStart + 1; + + // continue parsing just after the escape char + text = textStart + 1; + } + + break; + } + + case 'C': + { + curNode->type = OPTION; + curNode->option.type = COLOR; + // pass type so we don't actually check the value + if (PARENT_IS_SAME_OPTION(type)) + { + ++text; + // this is correct, there is a color to be reverted + // we don't need a new node, but we do need to move the current node from being a child of the color node + // to being a sibling + + // change the parent's next node to this + // we know here that the parent will have no other children, because we only move forward + action = GO_TO_PARENT; + } + else + { + // we have no color to revert, so just treat this as text + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = textStart + 1; + } + break; + } + + // alignment (LCR TMB) + // alignment (<|> ^-v) + case 'l': + { + curNode->type = OPTION; + curNode->option.type = ALIGN; + action = ADD_CHILD; + + switch (*(++text)) + { + case 'L': + case '<': + curNode->option.align = ALIGN_LEFT; + break; + + case 'C': + case '|': + curNode->option.align = ALIGN_CENTER; + break; + + case 'R': + case '>': + curNode->option.align = ALIGN_RIGHT; + + case 'T': + case '^': + curNode->option.align = VALIGN_TOP; + break; + + case 'M': + case '-': + curNode->option.align = VALIGN_CENTER; + break; + + case 'B': + case 'v': + curNode->option.align = VALIGN_BOTTOM; + break; + + default: + { + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = text + 1; + action = ADD_SIBLING; + break; + } + } + + ++text; + break; + } + + case 'L': + { + curNode->type = OPTION; + curNode->option.type = ALIGN; + if (PARENT_IS_SAME_OPTION(type)) + { + ++text; + action = GO_TO_PARENT; + } + else + { + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = ++text; + action = ADD_SIBLING; + } + break; + } + + case 'd': + { + bool valid = false; + if (*(++text) == '(') + { + do { + ++text; + } while (*text != ')' && *text != '\0' && *text != '\n'); + + if (*text == ')') + { + valid = true; + } + } + + if (valid) + { + // found matching paren + curNode->type = OPTION; + curNode->option.type = FONT; + // textStart is '\', textStart+1 is 'd', textStart+2 is '(', so textStart+3 is our font name + curNode->option.font.start = textStart + 3; + + curNode->option.font.end = text; + + ++text; + action = ADD_CHILD; + } + else + { + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = ++text; + action = ADD_SIBLING; + } + break; + } + + case 'D': + { + curNode->type = OPTION; + curNode->option.type = FONT; + if (PARENT_IS_SAME_OPTION(type)) + { + // be responsible and clean up this mess + curNode->type = EMPTY; + ++text; + action = GO_TO_PARENT; + } + else + { + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = ++text; + } + break; + } + + // not sure what they're trying to escape + // so, just treat it as regular text + default: + { + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = ++text; + break; + } + } + + break; + } + + case '#': + { + if (START_OF_LINE()) + { + MDLOG("Start of line, using header"); + curNode->type = DECORATION; + curNode->decoration = HEADER; + while (*(++text) == ' '); + + action = ADD_CHILD; + } + else + { + MDLOG("Not at start of line, marking as text"); + curNode->type = TEXT; + curNode->text.start = text; + curNode->text.end = ++text; + } + break; + } + + case '_': + case '*': + case '~': + { + while (*(++text) == *textStart); + + curNode->type = OPTION; + curNode->option.type = STYLE; + + // (check if *text == '\n' and lastNode is LINE_BREAK or PARAGRAPH_BREAK) + if (text - textStart > 2 && *textStart == '~') + { + text = textStart + 2; + } + + // ONE CHAR + // - Underscore, Asterisk: Italics + // - Tilde: Underline (*** Not Standard ***) + // TWO CHAR + // - Underscore, Asterisk: Bold + // - Tilde: Strikethrough + // THREE CHAR + // - Underscore, Asterisk: Bold & Italics + + if (text - textStart == 1) + { + if (*textStart == '~') + { + curNode->option.style = STYLE_UNDERLINE; + } + else + { + curNode->option.style = STYLE_ITALIC; + } + } + else if (text - textStart == 2) + { + if (*textStart == '~') + { + curNode->option.style = STYLE_STRIKE; + } + else + { + curNode->option.style = STYLE_BOLD; + } + } + else if (text - textStart == 3) + { + // if we're here, *textStart cannot be '~' + if (*text == '\n' && START_OF_LINE()) + { + curNode->type = DECORATION; + curNode->decoration = HORIZONTAL_RULE; + } + else + { + curNode->option.style = STYLE_ITALIC | STYLE_BOLD; + } + } + + if (curNode->type == OPTION && PARENT_IS_SAME_OPTION(style)) + { + curNode->type = EMPTY; + action = GO_TO_PARENT; + } + else if (curNode->type == DECORATION) + { + action = ADD_SIBLING; + } + else + { + action = ADD_CHILD; + } + break; + } + + case '-': + { + while (*(++text) == *textStart); + + if (text - textStart >= 3 && *text == '\n' && START_OF_LINE()) + { + curNode->type = DECORATION; + curNode->decoration = HORIZONTAL_RULE; + } + else + { + // TODO: Support underlined headers + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = text; + } + break; + } + + case '!': + { + bool valid = false; + + // TODO replace this with something that's not incredibly slow + if (*(++text) == '[') + { + do { + ++text; + } while ((*text != ']' || *(text - 1) == '\\') && *text != '\0' && *text != '\n'); + + if (*text == ']') + { + // that was the alt text + if (*(++text) == '(') + { + curNode->image.start = text + 1; + + do { + ++text; + } while ((*text != ')' || *(text - 1) == '\\') && *text != '\0' && *text != '\n'); + + if (*text == ')') + { + valid = true; + curNode->image.end = text; + text++; + } + } + } + } + + if (valid) + { + curNode->type = IMAGE; + } + if (!valid) + { + curNode->type = TEXT; + curNode->text.start = textStart; + curNode->text.end = textStart + 1; + text = textStart + 1; + } + + break; + } + + case '\n': + { + // If there's one newline, pretend it's a space + // If there's more than one newline, collapse them all into max two + int nls = 1; + while (*(++text) == '\n') + { + nls++; + } + + curNode->type = DECORATION; + if (nls > 1) + { + curNode->decoration = PARAGRAPH_BREAK; + } + else + { + // check if the last line ended with two spaces + if (lastNode != NULL && lastNode->type == TEXT && (lastNode->text.end - lastNode->text.start) >= 2 + && !strncmp(lastNode->text.end - 2, " ", 2)) + { + curNode->decoration = LINE_BREAK; + } + else + { + curNode->decoration = WORD_BREAK; + } + } + + // TODO: Check if the last token was actually a newline + + if (curNode->parent != NULL + && curNode->parent->type == DECORATION + && curNode->parent->decoration == HEADER) + { + // if last node was a header, break out of the header and also add the newline + action = GO_TO_PARENT | ADD_SIBLING; + } + + break; + } + + + default: + { + // this is regular text, produce a text node + curNode->type = TEXT; + curNode->text.start = text; + + // Advance to the next char we need to handle + text = strpbrk(text + 1, "\\_*~#-\n!"); + + if (text == NULL) + { + // end of string! + text = curNode->text.start + strlen(curNode->text.start); + } + + // end is exclusive so set it to the next char pointer + curNode->text.end = text; + break; + } + } + + if (action & GO_TO_PARENT) + { + // so what should happen here... + // is, instead of allocating a new node and attaching it as a child/sibling + // we will keep curNode the same, and move it so that it becomes the first sibling + // of its parent + // also, we need to make sure that the previous node's next is not set to us anymore + // and we aren't doing that, so that's probably why Bad Things are happening + // this effectively "pops out" a level of the parse stack + + MDLOG("Moving to parent\n"); + + MDLOG("Old parent:\n"); + + printNode(curNode->parent, 2); + + MDLOG("Old parent next:\n"); + printNode(curNode->parent->next, 4); + + MDLOG("Current node:\n"); + printNode(curNode, 2); + if (curNode == curNode->parent) + { + MDLOG("This node is its own child, oh dear\n"); + } + + if (curNode->parent->child == curNode) + { + // we are the first child of our parent + // this means our parent will no longer have a child + // man these comments are getting weird + MDLOG("Child removed"); + curNode->parent->child = NULL; + } + + if (lastNode->next == curNode) + { + // we are someone's sibling + // we should not be, because we are now their... aunt/uncle...... + lastNode->next = NULL; + } + + // okay, now make our + lastNode = curNode->parent; + lastNode->next = curNode; + + // finally, set our parent to our new parent's parent + if (curNode->parent != NULL) + { + curNode->parent = curNode->parent->parent; + } + +#ifdef DEBUG + MDLOG("New parent:\n"); + printNode(curNode->parent, 2); + + if (curNode->parent != NULL) + { + MDLOG("New parent next:\n"); + printNode(curNode->parent->next, 4); + } + + if (lastNode != NULL) + { + // MDLOG("Old parent next:\n"); + printNode(lastNode->next, 2); + } +#endif + } + + if (action & ADD_SIBLING) + { + // continue with next node as a sibling + if (!mergeTextNodes(&curNode, &lastNode)) + { + curNode = newNode(out, curNode->parent, lastNode = curNode); + curNode->index = ++index; + } + } + else if (action & ADD_CHILD) + { + curNode = newNode(out, curNode, lastNode = NULL); + curNode->index = ++index; + } + } + + MDLOG("Dangling(?) node: \n"); + + printNodeDetailed(curNode, 0); + + // The last node is dangling -- we already set up the references when it was created + // but it is no longer needed and should be freed now + // remove it from its previous sibling + if (curNode->type == EMPTY) + { + if (lastNode != NULL && lastNode->next == curNode) + { + lastNode->next = NULL; + } + + // remove it from its parent, if any + if (curNode->parent != NULL && curNode->parent->child == curNode) + { + curNode->parent->child = NULL; + } + + if (curNode->child != NULL && curNode->child->parent == curNode) + { + curNode->child->parent = NULL; + } + + MDLOG("Freeing dangling node\n"); + free(curNode); + } +} + +markdownText_t* parseMarkdown(const char* text) +{ + MDLOG("Preparing to parse markdown\n"); + + _markdownText_t* result = calloc(1, sizeof(_markdownText_t)); + + result->text = text; + + MDLOG("Allocated stuff\n"); + parseMarkdownInner(text, result); + + return result; +} + +static int stackFrames = 0; + +static void freeSiblings(mdNode_t* tree) +{ + stackFrames++; + + MDLOG("Stack level: %d\n", stackFrames); + + mdNode_t* tmp; + while (tree != NULL) + { + if (tree->child != NULL) + { + freeTree(tree->child); + } + + tmp = tree->next; + free(tree); + tree = tmp; + } + stackFrames--; +} + +static void freeTree(mdNode_t* tree) +{ + stackFrames++; + + MDLOG("Stack level: %d\n", stackFrames); + + // don't use any local variables to hopefully prevent stack overflow + // otherwise this function would be a pain to implement + + //printNode(tree, indent); + + // first, recursively delete all this node's children + if (tree->child != NULL) + { + freeTree(tree->child); + + } + + // then, iteratively delete this node's next node + // but only if this is the first child of the parent + + if (tree->next != NULL) + { + freeSiblings(tree->next); + } + + // finally, if the node isn't the root node, delete itself + free(tree); + + stackFrames--; +} + +void freeMarkdown(markdownText_t* markdown) +{ + _markdownText_t* ptr = markdown; + + MDLOG("Freeing Markdown\n------------\n\n"); + if (ptr != NULL) + { + freeTree(ptr->tree); + free(ptr); + } +} + +static const mdOpt_t* findPreviousOption(const mdNode_t* node, mdOptType_t type) +{ + while (node->parent != NULL) + { + node = node->parent; + if (node->type == OPTION && node->option.type == type) + { + return &(node->option); + } + else if (type == ALIGN && node->type == DECORATION && node->decoration == HEADER) + { + return &headerOpts; + } + } + + return NULL; +} + +static const textStyle_t findPreviousStyles(const mdNode_t* node, mdPrintState_t* state) +{ + textStyle_t style = state->defaults.style; + while (node->parent != NULL) + { + node = node->parent; + if (node->type == OPTION && node->option.type == STYLE) + { + style |= node->option.style; + } + } + + return style; +} + +static const font_t* findPreviousFont(const mdNode_t* node, mdPrintState_t* state) +{ + mdText_t* font = NULL; + while (node->parent != NULL) + { + node = node->parent; + + if (node->type == OPTION && node->option.type == FONT) + { + font = &(node->option.font); + break; + } + else if (node->type == DECORATION && node->decoration == HEADER) + { + return state->params.headerFont; + } + } + + if (font != NULL) + { + MDLOG("I don't know what font to return!!!\n"); + // TODO + } + + return state->defaults.bodyFont; +} + +static void leavingNode(const mdNode_t* node, mdPrintState_t* state) +{ + // handle any actions we need to take when leaving the current node + // i.e. if we're leaving an option node, search up the tree to find the previous value for whatever option + switch (node->type) + { + case DECORATION: + { + switch (node->decoration) + { + case HEADER: + { + //state->y += textLineHeight(state->font, state->params.style); + //state->x = state->params.xMin; + state->font = findPreviousFont(node, state); + mdOpt_t* newOption = findPreviousOption(node, ALIGN); + state->params.align = (newOption != NULL) ? newOption->align : state->defaults.align; + break; + } + + default: + break; + } + break; + } + + case OPTION: + { + if (node->option.type == FONT) + { + state->font = findPreviousFont(node, state); + } + else if (node->option.type == STYLE) + { + // Just set the new style + state->params.style = findPreviousStyles(node, state); + } + else + { + mdOpt_t* option = findPreviousOption(node, node->option.type); + switch (node->option.type) + { + case ALIGN: + state->params.align = option ? option->align : state->defaults.align; + break; + + case BREAK: + state->params.breakMode = option ? option->breakMode : state->defaults.breakMode; + break; + + case COLOR: + state->params.color = option ? option->color : state->defaults.color; + break; + + case STYLE: + case FONT: + // already handled + break; + } + } + } + + default: + break; + } +} + +static mdLinePartInfo_t* pushLinePart(mdPrintState_t* state) +{ + // add a new mdLinePlan_t onto state->linePlan.parts + if (state->linePlan.partCount >= state->linePlan.partAlloc) + { + state->linePlan.partAlloc += 4; + state->linePlan.parts = realloc(state->linePlan.parts, sizeof(mdLinePartInfo_t) * state->linePlan.partAlloc); + } + + state->linePlan.pending = true; + return state->linePlan.parts + (state->linePlan.partCount)++; +} + +static void navigateToNode(const mdNode_t* tree, size_t index, const mdNode_t** nodeOut, const mdNode_t** prevOut) +{ + *prevOut = NULL; + *nodeOut = tree; + + while ((*nodeOut) != NULL && (*nodeOut)->index != index) + { + if ((*nodeOut)->next != NULL && (*nodeOut)->next->index <= index) + { + *prevOut = *nodeOut; + *nodeOut = (*nodeOut)->next; + } + else if ((*nodeOut)->child != NULL) + { + *prevOut = NULL; + *nodeOut = (*nodeOut)->child; + } + else + { + *nodeOut = NULL; + *prevOut = NULL; + } + } + + return; +} + +static void resetPlan(mdLinePlan_t* plan) +{ + plan->totalWidth = 0; + plan->partCount = 0; + plan->pending = false; + plan->lineHeight = 0; + plan->firstTextOffset = 0; +} + +static bool drawPlannedLine(display_t* disp, mdPrintState_t* state) +{ + if (state->linePlan.pending && state->linePlan.partCount > 0) + { + MDLOG("\nDrawing planned line with %zu parts, width %d, and height %d!\n", state->linePlan.partCount, state->linePlan.totalWidth, state->linePlan.lineHeight); + state->linePlan.pending = false; + + int16_t startX = state->x; + int16_t startY = state->y; + + // Set up the initial X depending on the alignment setting + if ((state->linePlan.align & ALIGN_CENTER) == ALIGN_CENTER) + { + startX = (state->params.xMax - state->params.xMin - state->linePlan.totalWidth) / 2 + state->params.xMin; + MDLOG("Centering text with offset %d\n", startX); + } + else if ((state->linePlan.align & ALIGN_RIGHT) == ALIGN_RIGHT) + { + startX = state->params.xMax - state->linePlan.totalWidth; + MDLOG("Right-aligning text with offset %d\n", startX); + } + else + { + MDLOG("Left-aligning text with offset %d\n", startX); + } + + int16_t x = startX; + int16_t y = startY; + int16_t yOffset; + + for (size_t i = 0; i < state->linePlan.partCount; i++) + { + mdLinePartInfo_t* part = state->linePlan.parts + i; + + switch (part->node->type) + { + case TEXT: + { + // Center the text verticlaly within the line + // To align the text along the baseline insead, we can simply subtract the part height from the line height + yOffset = (state->linePlan.lineHeight - textLineHeight(part->font, part->textStyle)) / 2; + + if (i > 0 && ((part - 1)->textStyle & STYLE_ITALIC) && !(part->textStyle & STYLE_ITALIC)) + { + // We have switched from italics to non-italics! + // We must finally pay for those extra pixels we were borrowing from the next character... + // TODO we also have to do this when we measure.. + x += textWidthAttrs((part - 1)->font, "", (part - 1)->textStyle); + } + + // Account for the offset in Y + y = startY + yOffset; + + // Draw the text. No need to save the return value as the text will definitely 100% fit for sure + drawTextWordWrapExtra(disp, part->font, part->color, + // If this is the first node in the line, we may need to start drawing at an offset + // due to partial printing on the previous line + (i == 0 ? state->linePlan.firstTextOffset : 0) + part->node->text.start, + // Actual X and Y, since we're really drawing + &x, &y, + state->params.xMin, state->params.yMin, + // Actually, the last line won't fit! So only draw to the end of the line + // We still don't need to save the return value though, because we double checked and are sure! + state->params.xMax, y + state->linePlan.lineHeight, + part->textStyle, part->node->text.end); + + // Undo the yOffset so y is the start of the line again + // TODO: Do we actually need to keep the Y value? It shouldn't change since we won't actually wrap lines. Or should it? + y -= yOffset; + + break; + } + + case DECORATION: + { + if (part->node->decoration == WORD_BREAK) + { + if (x != startX) + { + x += part->font->chars[0].w + 1; + } + } + break; + } + + case IMAGE: + { + + break; + } + + default: + break; + } + } + + state->x = state->params.xMin; + state->y += state->linePlan.lineHeight; + + resetPlan(&state->linePlan); + + // Return whether we actually drew a line + return true; + } + + // Return false, indicating no line needed to be drawn + // (maybe the caller needs to know if they should add a newline?) + return false; +} + +static const char* planLine(display_t* disp, const mdNode_t* node, mdPrintState_t* state) +{ + // How it works: + // - First, we measure one line of the text + // - If 100% of the text fits on a line, we don't draw it + // - Instead, we add it to a list of (text, textAttrs, width, lineHeight) in the state + // - UGH, what to do about word break tokens... I guess also keep those in there + // - Then, once we reach the end of the line, we can print! + // - If we're handling alignment, we can measure the length of the entire line at once and + // God, this is going to make the printing even slower... that's 2+ word wrap calls per text... + + MDLOG("Planning line with initial offset %zu\n", state->textPos); + + // Check if the previous part was in italics, and this one isn't + if (state->linePlan.partCount > 0 && (state->linePlan.parts[state->linePlan.partCount - 1].textStyle & TEXT_ITALIC) && !(state->params.style & TEXT_ITALIC)) + { + int16_t extraWidth = textWidthAttrs(state->linePlan.parts[state->linePlan.partCount - 1].font, "", state->linePlan.parts[state->linePlan.partCount - 1].textStyle); + state->linePlan.totalWidth += extraWidth; + MDLOG("Adding %d extra space to line plan for italics, total=%d\n", extraWidth, state->linePlan.totalWidth); + } + + // Handle word breaks + if (node->type == DECORATION && node->decoration == WORD_BREAK) + { + // For a word break, we can just ignore it if we're at the start of the line + if (state->linePlan.pending) + { + state->linePlan.totalWidth += state->font->chars[0].w + 1; + + // We need a new line part either way + mdLinePartInfo_t* part = pushLinePart(state); + + // Attach this node to the part + part->node = node; + part->textStyle = state->params.style; + part->color = state->params.color; + part->font = state->font; + } + + // We really don't care if this makes the width exceed the actual max just yet + // The next time we try to plan a line, we will immediately find that the text + // does not fit and draw the planned line. Also, we can't really + + return NULL; + } + else if (node->type == TEXT) + { + // TODO: I think there's something wrong with state->textPos here... + // We can discard textPos as soon as we initialize it here + // But then we use it to set firstTextOffset = + const char* remainingText = node->text.start + state->textPos; + state->textPos = 0; + + // TODO: OPTIMIZATION! + // - Measure the entire text using `drawTextWordWrapExtra(NULL, ...)` + // - Because the height is uniform, we can just determine the number of lines to draw + // - Measure and add the remainder to the plan + // - Draw the planned line as usual + // - Then draw all but the last remaining line without involving the line planner + // - (but then we can't do centering... worry about thta later, it's not too much harder) + // - Add the remainder to the plan as usual + + while (remainingText != NULL) + { + int16_t lineHeight = textLineHeight(state->font, state->params.style); + const char* textStart = remainingText; + + // We're drawing after all the existing text, so take that into account + // If there's no pending line, this will be zero + int16_t x = state->linePlan.totalWidth; + // We're just measuring the height here + int16_t y = 0; + + MDLOG("Measuring text of length %zu with Y bounds (0, %d)\n", node->text.end - node->text.start, lineHeight); + + // We need to check if the text we're planning to draw will go past the end of the existing line + // After this, remainingText will point to all text that did not fit on the line + // And `x` will either point to the + remainingText = drawTextWordWrapExtra(NULL, state->font, state->params.color, remainingText, + &x, &y, 0, 0, + state->params.xMax - state->params.xMin, lineHeight, + state->params.style, node->text.end); + + MDLOG("Measured %zu characters: '%s'\n", (remainingText == NULL ? node->text.end - textStart : remainingText - textStart), + strndebug(textStart, remainingText == NULL ? node->text.end : remainingText)); + + // If this will be the first part of the line, set the initial parameters + if (!state->linePlan.pending) + { + MDLOG("First part of line, setting firstTextOffset=%zu and align=%d\n", state->textPos, state->params.align); + state->linePlan.firstTextOffset = textStart - node->text.start; + state->linePlan.align = state->params.align; + } + + // We need a new line part either way + mdLinePartInfo_t* part = pushLinePart(state); + + // Attach this node to the part + part->node = node; + part->textStyle = state->params.style; + part->color = state->params.color; + part->font = state->font; + + // Since we started measuring at the original totalWidth, x will already be set to the new value + // TODO We should be able to use state->linePlan.totalWidth instead of x + MDLOG("Planned width is %d, original is %d (added %d)\n", x, state->linePlan.totalWidth, x - state->linePlan.totalWidth); + MDLOG("Y shouldn't have changed, it was %d and is now %d\n", 0, y); + if (x > state->linePlan.totalWidth) + { + state->linePlan.totalWidth = x; + } + else + { + state->linePlan.totalWidth += textWidthExtra(state->font, textStart, state->params.style, remainingText); + MDLOG("Adding partial line length, now %d\n", state->linePlan.totalWidth); + } + + // Check if some or all of the text fits + if (remainingText == NULL || (remainingText != node->text.start)) + { + // More text fits, now we need to update the line height! + if (lineHeight > state->linePlan.lineHeight) + { + MDLOG("Increasing planned line height from %d to %d\n", state->linePlan.lineHeight, lineHeight); + state->linePlan.lineHeight = lineHeight; + + // Check if the line still fits... + if (state->y + state->linePlan.lineHeight > state->params.yMax) + { + // The line no longer fits! WE HAVE TO GO BACK + // We know there's a first part since we have just added ourselves + state->backtrackIndex = state->linePlan.parts[0].node->index; + + MDLOG("Line must be backtracked to node %d, offset %zu due to height change (%d + %d > %d)\n", + state->backtrackIndex, state->linePlan.firstTextOffset, + state->y, state->linePlan.lineHeight, state->params.yMax); + + // I don't think we actually change textPos, so this may be completely unnecessary + // (we also don't really use the actual return value anymore either) + return node->text.start + (state->textPos = state->linePlan.firstTextOffset); + } + } + } + + // If any of the text didn't fit on this line, the line is done + if (remainingText != NULL) + { + MDLOG("Line is complete! Drawing\n\n"); + drawPlannedLine(disp, state); + } + + if (state->y + lineHeight - 1 > state->params.yMax) + { + MDLOG("Returning early from planLine because height exceeds bounds: %d + %d > %d\n", state->y, lineHeight, state->params.yMax); + state->textPos = remainingText - node->text.start; + return remainingText; + } + } + + // Return NULL to indicate that all this node's text was successfully draw + return NULL; + } + else if (node->type == IMAGE) + { + // TODO + } + + return NULL; +} + +static bool drawMarkdownNode(display_t* disp, const mdNode_t* node, const mdNode_t* prev, mdPrintState_t* state) +{ + switch (node->type) + { + case TEXT: + { + const char* remain = planLine(disp, node, state); + + if (remain != NULL) + { + // not everything fit on the screen + // reset the position and exit + state->x = state->params.xMin; + state->y = state->params.yMin; + + // don't save the return value as state->textPos anymore, + // since we may have to deal with backtracking + + // return true to indicate there's more pages to be drawn + return true; + } + + state->textPos = 0; + + break; + } + + case OPTION: + { + switch (node->option.type) + { + case STYLE: + { + state->params.style |= node->option.style; + break; + } + + case ALIGN: + { + state->params.align = node->option.align; + break; + } + + case BREAK: + { + state->params.breakMode = node->option.breakMode; + break; + } + + case COLOR: + { + state->params.color = node->option.color; + break; + } + + case FONT: + { + // TODO + break; + } + } + break; + } + + case DECORATION: + { + switch (node->decoration) + { + case HORIZONTAL_RULE: + { + drawPlannedLine(disp, state); + + if (disp != NULL) + { + plotLine(disp, state->params.xMin, state->y, state->params.xMax, state->y, state->params.color, 0); + } + } + break; + + case WORD_BREAK: + planLine(disp, node, state); + break; + + case LINE_BREAK: + { + if (!drawPlannedLine(disp, state)) + { + state->x = state->params.xMin; + state->y += textLineHeight(state->font, state->params.style); + } + break; + } + + case PARAGRAPH_BREAK: + { + if (drawPlannedLine(disp, state)) + { + // A line was drawn, so we only need to add one extra line + state->y += textLineHeight(state->font, state->params.style); + } + else + { + // There was no pending line to draw, so add two lines + state->x = state->params.xMin; + state->y += textLineHeight(state->font, state->params.style) * 2; + } + break; + } + + case PAGE_BREAK: + { + drawPlannedLine(disp, state); + state->x = state->params.xMax + 1; + state->y = state->params.yMax + 1; + break; + } + + case BULLET: + break; + + case HEADER: + { + drawPlannedLine(disp, state); + // Add a line break but only if we're not at the beginning of a line + // drawPlannedLine() + //state->x = state->params.xMin; + //state->y += textLineHeight(state->font, state->params.style); + state->font = state->params.headerFont; + state->params.align = ALIGN_CENTER; + break; + } + } + } + + default: + break; + } + + return false; +} + + +bool drawMarkdown(display_t* disp, const markdownText_t* markdown, const markdownParams_t* params, markdownContinue_t** pos, bool savePos) +{ + const mdNode_t* node = ((const _markdownText_t*)markdown)->tree; + const mdNode_t* prev = NULL; + + uint8_t indent = 0; + size_t index = 0; + + mdPrintState_t state = + { + .x = params->xMin, + .y = params->yMin, + .font = NULL, + .data = { NULL }, + .textPos = 0, + .backtrackIndex = NO_BACKTRACK, + }; + + memcpy(&state.params, params, sizeof(markdownParams_t)); + if (state.params.align == 0) + { + state.params.align = ALIGN_LEFT | VALIGN_TOP; + } + + memcpy(&state.defaults, &state.params, sizeof(markdownParams_t)); + + state.linePlan.partAlloc = 4; + state.linePlan.parts = MD_MALLOC(sizeof(mdLinePartInfo_t) * state.linePlan.partAlloc); + resetPlan(&state.linePlan); + + state.font = params->bodyFont; + + if (pos != NULL && *pos != NULL) + { + index = ((_markdownContinue_t*)*pos)->treeIndex; + state.textPos = ((_markdownContinue_t*)*pos)->textPos; + + navigateToNode(node, index, &node, &prev); + + mdOpt_t* lastOpt = findPreviousOption(node, ALIGN); + if (lastOpt != NULL) + { + state.params.align = lastOpt->align; + } + + lastOpt = findPreviousOption(node, BREAK); + if (lastOpt != NULL) + { + state.params.breakMode = lastOpt->breakMode; + } + + lastOpt = findPreviousOption(node, COLOR); + if (lastOpt != NULL) + { + state.params.color = lastOpt->color; + } + + state.params.style = findPreviousStyles(node, &state); + state.font = findPreviousFont(node, &state); + } + + MDLOG("Printing Markdown\n-----------\n\n"); + + while (node != NULL) + { + printNode(node, indent); + + // drawMarkdownNode() returns true if it has more to draw + if (drawMarkdownNode(disp, node, prev, &state)) + { + if (savePos && pos != NULL) + { + if (*pos == NULL) + { + *pos = malloc(sizeof(_markdownContinue_t)); + } + + ((_markdownContinue_t*)*pos)->treeIndex = (state.backtrackIndex == NO_BACKTRACK) ? index : state.backtrackIndex; + + // drawMarkdownNode() also writes its partial position to `state.textPos` + ((_markdownContinue_t*)*pos)->textPos = state.textPos; + } + // We drew as much as we could draw! Don't update the node! + + return true; + } + + ++index; + + if (node->child != NULL) + { + prev = NULL; + node = node->child; + indent++; + } + else if (node->next != NULL) + { + leavingNode(node, &state); + prev = node; + node = node->next; + } + else { + // move up the parent tree until one of them has a next + while (node != NULL) + { + leavingNode(node, &state); + indent--; + node = node->parent; + if (node != NULL && node->next != NULL) + { + leavingNode(node, &state); + prev = node; + node = node->next; + break; + } + } + } + } + + // Draw the remaining planned line if there is one + drawPlannedLine(disp, &state); + + if (savePos && pos != NULL) + { + if (*pos != NULL) + { + free(*pos); + *pos = NULL; + } + } + + free(state.linePlan.parts); + + return false; +} + +markdownContinue_t* copyContinue(const markdownContinue_t* pos) +{ + if (pos == NULL) + { + return NULL; + } + + markdownContinue_t* result = malloc(sizeof(_markdownContinue_t)); + memcpy(result, pos, sizeof(_markdownContinue_t)); + + return result; +} \ No newline at end of file diff --git a/main/utils/markdown_parser.h b/main/utils/markdown_parser.h new file mode 100644 index 00000000..f07e9e38 --- /dev/null +++ b/main/utils/markdown_parser.h @@ -0,0 +1,65 @@ +#ifndef _MARKDOWN_PARSER_H_ +#define _MARKDOWN_PARSER_H_ + +#include +#include + +#include "display.h" +#include "font.h" +#include "palette.h" + +typedef enum +{ + STYLE_NORMAL = 0, + STYLE_ITALIC = 1, + STYLE_BOLD = 2, + STYLE_UNDERLINE = 4, + STYLE_STRIKE = 8, +} textStyle_t; + +typedef enum +{ + ALIGN_LEFT = 1, + ALIGN_RIGHT = 2, + ALIGN_CENTER = 3, + + VALIGN_TOP = 4, + VALIGN_BOTTOM = 8, + VALIGN_CENTER = 12, +} textAlign_t; + +/// @brief Defines the strategy to be used when the text to be printed does not fit on a single line +typedef enum +{ + /// @brief Text will be broken onto the next line at word boundaries (' ' or '-') if possible + BREAK_WORD, + /// @brief Text will be broken onto the next line immediately after the last character that fits + BREAK_CHAR, + /// @brief Text will be printed up to the last character that fits, and the rest will be ignored. + BREAK_TRUNCATE, + /// @brief Text will be printed up to the last word that fits, and the rest will be ignored + BREAK_TRUNCATE_WORD, + /// @brief Text will not be drawn unless it fits completely on a single line + BREAK_NONE, +} textBreak_t; + +typedef struct _markdownText_t *markdownText_t; +typedef struct _markdownContinue_t *markdownContinue_t; + +typedef struct +{ + int16_t xMin, yMin, xMax, yMax; + textStyle_t style; + textAlign_t align; + textBreak_t breakMode; + paletteColor_t color; + const font_t* bodyFont; + const font_t* headerFont; +} markdownParams_t; + +markdownText_t* parseMarkdown(const char* text); +void freeMarkdown(markdownText_t* markdown); +bool drawMarkdown(display_t* disp, const markdownText_t* markdown, const markdownParams_t* params, markdownContinue_t** pos, bool savePos); +markdownContinue_t* copyContinue(const markdownContinue_t* pos); + +#endif diff --git a/main/utils/text_entry.c b/main/utils/text_entry.c index 82adf86e..befa0373 100644 --- a/main/utils/text_entry.c +++ b/main/utils/text_entry.c @@ -4,6 +4,7 @@ #include "text_entry.h" #include "display.h" +#include "font.h" #include "bresenham.h" #include "btn.h" #include diff --git a/main/utils/text_entry.h b/main/utils/text_entry.h index b518cd5d..61efff4e 100644 --- a/main/utils/text_entry.h +++ b/main/utils/text_entry.h @@ -3,6 +3,7 @@ #include #include "display.h" +#include "font.h" void textEntryStart( display_t * usedisp, font_t * usefont, int max_len, char* buffer ); bool textEntryDraw( void ); diff --git a/spiffs_file_preprocessor/spiffs_file_preprocessor.c b/spiffs_file_preprocessor/spiffs_file_preprocessor.c index 3d4a53d1..d3f0f85c 100644 --- a/spiffs_file_preprocessor/spiffs_file_preprocessor.c +++ b/spiffs_file_preprocessor/spiffs_file_preprocessor.c @@ -73,7 +73,7 @@ static int processFile(const char * fpath, const struct stat * st __attribute__( { process_bin(fpath, outDirName); } - else if(endsWith(fpath, ".txt")) + else if(endsWith(fpath, ".txt") || endsWith(fpath, ".md")) { process_txt(fpath, outDirName); } diff --git a/spiffs_file_preprocessor/txt_processor.c b/spiffs_file_preprocessor/txt_processor.c index 2a4f48a6..a3fe27f2 100644 --- a/spiffs_file_preprocessor/txt_processor.c +++ b/spiffs_file_preprocessor/txt_processor.c @@ -7,9 +7,14 @@ #include "heatshrink_encoder.h" #include "fileUtils.h" +#define MIN_PRINT_CHAR (' ') +#define MAX_PRINT_CHAR ('~' + 1) + +static bool validate_chars(const char* file, const char* str); + /** * @brief Removes all instances of a given char from a string. Modifies the string in-place and sets a new null terminator, if needed - * + * * @param str string to remove chars from * @param len number of chars in the string, including null terminator * @param c char to remove @@ -29,6 +34,35 @@ long remove_chars(char* str, long len, char c) { return newLen; } +static bool validate_chars(const char* file, const char* str) +{ + bool ok = true; + long lineNum = 1; + long byteNum = 0; + while (*str) + { + ++byteNum; + if (*str == '\n') + { + ++lineNum; + byteNum = 0; + } + else if (*str < 0) + { + fprintf(stderr, "ERROR: Unprintable character \\x%02X at %s:%ld:%ld\n", 256 + (*str), file, lineNum, byteNum); + } + else if (*str < MIN_PRINT_CHAR || *str > MAX_PRINT_CHAR) + { + fprintf(stderr, "ERROR: Unprintable character \\x%02X at %s:%ld:%ld\n", *str, file, lineNum, byteNum); + ok = false; + } + + ++str; + } + + return ok; +} + void process_txt(const char *infile, const char *outdir) { /* Determine if the output file already exists */ @@ -54,6 +88,8 @@ void process_txt(const char *infile, const char *outdir) long newSz = remove_chars(txtInStr, sz, '\r'); fclose(fp); + validate_chars(infile, txtInStr); + /* Write input directly to output */ FILE* outFile = fopen(outFilePath, "wb"); fwrite(txtInStr, newSz, 1, outFile);