From 56e07e1080f2e0389856cb17a94a92b7053cf8dc Mon Sep 17 00:00:00 2001 From: nedko Date: Thu, 4 Dec 2025 17:23:53 +0200 Subject: [PATCH] Added WidgetText. Fixed some things. Updated README --- Makefile | 1 + README.md | 8 +- Widgets/Widget.h | 2 +- Widgets/WidgetText.cpp | 223 +++++++++++++++++++++++++++++++++++++++++ Widgets/WidgetText.h | 71 +++++++++++++ main.cpp | 5 +- sdl_helpers.cpp | 115 +++++++++++++++++++++ sdl_helpers.h | 56 ++++++++++- 8 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 Widgets/WidgetText.cpp create mode 100644 Widgets/WidgetText.h diff --git a/Makefile b/Makefile index 1ef9927..f7117bc 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,7 @@ src_files += main.cpp src_files += sdl_helpers.cpp src_files += Widgets/Widget.cpp +src_files += Widgets/WidgetText.cpp all: g++ $(src_files) -Wall -lSDL2 -lSDL2_ttf -lSDL2_image -o trmnl_sdl diff --git a/README.md b/README.md index 3968d69..795c28c 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,17 @@ A utility which can be used to create custom images for TRMNL devices via code # How to write new visual stuff 1. Inherit from `Widget` and then do your magic inside your own class. -2. Add your widget builder in `main.cpp` inside `init builders`. -3. Write your config file. +2. Add your widget builder in `main.cpp` inside `init_builders`. +3. Write your config file. Either named `config.json` or added as the first command line parameter. 4. ??? 5. Profit # ImageMagick commands ### Convert image without dithering -`magick input.png -monochrome -colors 2 -depth 1 -strip png:output.png` +`convert trmnl.png -monochrome -colors 2 -depth 1 -strip png:output.png` ### Convert image with dithering -`magick input.png -dither FloydSteinberg -remap pattern:gray50 -depth 1 -strip png:output.png` +`convert trmnl.png -dither FloydSteinberg -remap pattern:gray50 -depth 1 -strip png:output.png` # Notes json.hpp from nlohmann/json v3.11.2 diff --git a/Widgets/Widget.h b/Widgets/Widget.h index fc7dd85..2a3c2ed 100644 --- a/Widgets/Widget.h +++ b/Widgets/Widget.h @@ -5,7 +5,7 @@ class Widget { -private: +protected: // Internal surface for drawing SDL_Surface* m_surface; diff --git a/Widgets/WidgetText.cpp b/Widgets/WidgetText.cpp new file mode 100644 index 0000000..4a3e441 --- /dev/null +++ b/Widgets/WidgetText.cpp @@ -0,0 +1,223 @@ +#include "WidgetText.h" + +using std::string; + +static SDL_Surface* render_text(bool should_wrap, TTF_Font* font, const string& text, SDL_Color color, Uint32 wrap_size) +{ + SDL_Surface* txt_surface = nullptr; + if (should_wrap) + { + txt_surface = TTF_RenderUTF8_Solid_Wrapped(font, text.c_str(), color, wrap_size); + } + else + { + txt_surface = TTF_RenderUTF8_Solid(font, text.c_str(), color); + } + + return txt_surface; +} + +WidgetText::WidgetText(int width, int height, string text, + TextFit fit, bool should_wrap, + HorizontalAlign halign, VerticalAlign valign, + SDL_Color text_color, + int size, std::string font) +: Widget(width, height), +m_text(text), +m_font_file(font), +m_size(size), +m_fit(fit), +m_should_wrap(should_wrap), +m_halign(halign), +m_valign(valign), +m_text_color(text_color) +{ + if (nullptr != m_surface) + { + SDL_SetSurfaceBlendMode(m_surface, SDL_BLENDMODE_BLEND); + } +} + +WidgetText::WidgetText(int width, int height, string text, + TextFit fit, bool should_wrap, + int size, string font) +: Widget(width, height), +m_text(text), +m_font_file(font), +m_size(size), +m_fit(fit), +m_should_wrap(should_wrap), +m_halign(HALIGN_CENTER), +m_valign(VALIGN_CENTER), +m_text_color{.r = 0, .g = 0, .b = 0, .a = SDL_ALPHA_OPAQUE} +{ + if (nullptr != m_surface) + { + SDL_SetSurfaceBlendMode(m_surface, SDL_BLENDMODE_BLEND); + } +} + +WidgetText::WidgetText(int width, int height, string text, + int size, string font) +: Widget(width, height), +m_text(text), +m_font_file(font), +m_size(size), +m_fit(FIT_NONE), +m_halign(HALIGN_CENTER), +m_valign(VALIGN_CENTER), +m_text_color{.r = 0, .g = 0, .b = 0, .a = SDL_ALPHA_OPAQUE} +{ + if (nullptr != m_surface) + { + SDL_SetSurfaceBlendMode(m_surface, SDL_BLENDMODE_BLEND); + } +} + +void WidgetText::set_text(const string& text) +{ + m_text = text; +} + +void WidgetText::set_font(const string& font_file) +{ + m_font_file = font_file; +} + +void WidgetText::set_font_size(int size) +{ + m_size = size; +} + +void WidgetText::set_fit(TextFit fit) +{ + m_fit = fit; +} + +void WidgetText::set_halign(HorizontalAlign halign) +{ + m_halign = halign; +} + +void WidgetText::set_valign(VerticalAlign valign) +{ + m_valign = valign; +} + +void WidgetText::set_color(SDL_Color text_color) +{ + m_text_color = text_color; +} + +void WidgetText::draw() +{ + // Clear surface + SDL_FillRect(m_surface, nullptr, SDL_MapRGBA(m_surface->format, 255, 255, 255, SDL_ALPHA_TRANSPARENT)); + + string font_name = m_font_file; + if (font_name.empty()) + { + font_name = default_font_name; + } + + TTF_Font* font = get_font(m_size, font_name); + if (nullptr == font) + { + // TODO: Message printing + return; + } + + // Render text + SDL_Surface* txt_surface = render_text(m_should_wrap, font, m_text, m_text_color, m_rect.w); + + if (nullptr == txt_surface) + { + // TODO: Message printing + return; + } + + // Check if text fits + if (m_fit != FIT_NONE) + { + double x_scale; + double y_scale; + + x_scale = m_surface->w; + x_scale /= txt_surface->w; + + y_scale = m_surface->h; + y_scale /= txt_surface->h; + + // Find the scale needed to shrink or enlarge with + double min_scale = x_scale; + if (y_scale < min_scale) + { + min_scale = y_scale; + } + + // Do not scale up if only shrinkage is allowed + if ((min_scale > 1.0) && (FIT_SHRINK == m_fit)) + { + min_scale = 1.0; + } + + // Find new font size + int new_text_size = m_size; + new_text_size *= min_scale; + + // Re-render text + if (new_text_size != m_size) + { + SDL_FreeSurface(txt_surface); + font = get_font(new_text_size, font_name); + if (nullptr == font) + { + // TODO: Message printing + return; + } + txt_surface = render_text(m_should_wrap, font, m_text, m_text_color, m_rect.w); + if (nullptr == txt_surface) + { + // TODO: Message printing + return; + } + } + } + + // Now we have the final rendered text surface + SDL_Rect align = surface_align(m_surface, txt_surface, m_halign, m_valign); + SDL_BlitSurface(txt_surface, NULL, m_surface, &align); + SDL_FreeSurface(txt_surface); +} + +Widget* WidgetText::builder(const nlohmann::json& j) +{ + int width = 0; + int height = 0; + string text = ""; + TextFit fit = FIT_NONE; + bool should_wrap = false; + HorizontalAlign halign = HALIGN_CENTER; + VerticalAlign valign = VALIGN_CENTER; + SDL_Color text_color = {.r = 0, .g = 0, .b = 0, .a = SDL_ALPHA_OPAQUE}; + int size = 0; + string font = ""; + + json_extract(j, "width", width); + json_extract(j, "height", height); + json_extract(j, "text", text); + json_extract(j, "fit", fit); + json_extract(j, "should_wrap", should_wrap); + json_extract(j, "halign", halign); + json_extract(j, "valign", valign); + json_extract(j, "color", text_color); + json_extract(j, "size", size); + json_extract(j, "font", font); + + if ((0 == width) || (0 == height) || (0 == size)) + { + return nullptr; + } + + return new WidgetText(width, height, text, fit, should_wrap, halign, valign, text_color, size, font); +} diff --git a/Widgets/WidgetText.h b/Widgets/WidgetText.h new file mode 100644 index 0000000..5ff8b0b --- /dev/null +++ b/Widgets/WidgetText.h @@ -0,0 +1,71 @@ +#ifndef WIDGET_TEXT_H_ +#define WIDGET_TEXT_H_ + +#include "Widget.h" +#include + +#include + +#include "../sdl_helpers.h" +#include "../json.hpp" + +class WidgetText : public Widget +{ +protected: + // Text to display + std::string m_text; + + // Desired font file + // Leave empty for default + std::string m_font_file; + + // Desired font size + int m_size; + + // Whether to fit text inside rectangle + // FIT_SHRINK will try with the desired font size and shrink if needed + // FIT_AUTO will with the desired font size and shrink or enlarge if needed + // Default - FIT_NONE + TextFit m_fit; + + // Whether to wrap to multiple lines + // Default - false + bool m_should_wrap; + + // Default - center + HorizontalAlign m_halign; + + // Default - center + VerticalAlign m_valign; + + // Default - black + SDL_Color m_text_color; + +public: + WidgetText(int width, int height, std::string text, + TextFit fit, bool should_wrap, + HorizontalAlign halign, VerticalAlign valign, + SDL_Color text_color, + int size, std::string font = ""); + + WidgetText(int width, int height, std::string text, + TextFit fit, bool should_wrap, + int size, std::string font = ""); + + WidgetText(int width, int height, std::string text, + int size, std::string font = ""); + + void set_text(const std::string& text); + void set_font(const std::string& font_file); + void set_font_size(int size); + void set_fit(TextFit fit); + void set_halign(HorizontalAlign halign); + void set_valign(VerticalAlign valign); + void set_color(SDL_Color text_color); + + virtual void draw() override; + + static Widget* builder(const nlohmann::json& j); +}; + +#endif // WIDGET_TEXT_H_ diff --git a/main.cpp b/main.cpp index 66fee5d..ad3f6fd 100644 --- a/main.cpp +++ b/main.cpp @@ -11,6 +11,7 @@ #include "sdl_helpers.h" #include "Widgets/Widget.h" +#include "Widgets/WidgetText.h" using std::cout; using std::endl; @@ -21,7 +22,7 @@ using nlohmann::json; void init_builders(map& widget_builders) { - // widget_builders["name"] = &WidgetName::builder; + widget_builders["text"] = &WidgetText::builder; } int main(int argc, char **argv) @@ -107,7 +108,7 @@ int main(int argc, char **argv) } // Clear screen with white - SDL_FillRect(main_surface, nullptr, SDL_MapRGBA(main_surface->format, 255, 255, 255, 255)); + SDL_FillRect(main_surface, nullptr, SDL_MapRGBA(main_surface->format, 255, 255, 255, SDL_ALPHA_OPAQUE)); // Draw and apply all widgets for (Widget* widget : widgets) diff --git a/sdl_helpers.cpp b/sdl_helpers.cpp index a739fc1..efc9cad 100644 --- a/sdl_helpers.cpp +++ b/sdl_helpers.cpp @@ -11,6 +11,7 @@ using namespace std; using nlohmann::json; map, TTF_Font*> font_map; +string default_font_name = "font.ttf"; bool init_sdl() { @@ -75,6 +76,49 @@ TTF_Font* get_font(int size, const string& filename) } } +SDL_Rect surface_align(const SDL_Surface* base, const SDL_Surface* applied, + HorizontalAlign halign, VerticalAlign valign) +{ + SDL_Rect align = {.x = 0, .y = 0, .w = 0, .h = 0}; + + if ((nullptr == base) || (nullptr == applied)) + { + return align; + } + + switch (halign) + { + case HALIGN_LEFT: + align.x = 0; + break; + + case HALIGN_CENTER: + align.x = base->w / 2 - applied->w / 2; + break; + + case HALIGN_RIGHT: + align.x = base->w - applied->w; + break; + } + + switch (valign) + { + case VALIGN_TOP: + align.y = 0; + break; + + case VALIGN_CENTER: + align.y = base->h / 2 - applied->h / 2; + break; + + case VALIGN_BOTTOM: + align.y = base->h - applied->h; + break; + } + + return align; +} + bool read_config_json(json& cfg, const string& filename, ostream* log) { ifstream cfg_file(filename); @@ -120,3 +164,74 @@ void json_extract(const nlohmann::json& j, const string& key, int& out) out = j[key]; } } + +void json_extract(const nlohmann::json& j, const string& key, bool& out) +{ + if (j.contains(key) && j[key].is_boolean()) + { + out = j[key]; + } +} + +void json_extract(const json& j, const string& key, HorizontalAlign& out) +{ + if (j.contains(key)) + { + try + { + out = j[key].get(); + } + catch(const std::exception& e) + { + } + } +} + +void json_extract(const json& j, const string& key, VerticalAlign& out) +{ + if (j.contains(key)) + { + try + { + out = j[key].get(); + } + catch(const std::exception& e) + { + } + } +} + +void json_extract(const json& j, const string& key, TextFit& out) +{ + if (j.contains(key)) + { + try + { + out = j[key].get(); + } + catch(const std::exception& e) + { + } + } +} + +void json_extract(const json& j, const string& key, SDL_Color& out) +{ + if (j.contains(key) && j[key].is_object()) + { + const json& j2 = j[key]; + + json_extract(j2, "r", out.r); + json_extract(j2, "g", out.g); + json_extract(j2, "b", out.b); + json_extract(j2, "a", out.a); + } +} + +void json_extract(const nlohmann::json& j, const string& key, Uint8& out) +{ + if (j.contains(key) && j[key].is_number_unsigned()) + { + out = j[key]; + } +} diff --git a/sdl_helpers.h b/sdl_helpers.h index ac00c63..f2c1d63 100644 --- a/sdl_helpers.h +++ b/sdl_helpers.h @@ -8,7 +8,49 @@ #include "json.hpp" -std::string default_font_name = "font.ttf"; +extern std::string default_font_name; + +enum HorizontalAlign +{ + HALIGN_LEFT, + HALIGN_CENTER, + HALIGN_RIGHT +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(HorizontalAlign, +{ + {HALIGN_LEFT, "left"}, + {HALIGN_CENTER, "center"}, + {HALIGN_RIGHT, "right"}, +}) + +enum VerticalAlign +{ + VALIGN_TOP, + VALIGN_CENTER, + VALIGN_BOTTOM +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(VerticalAlign, +{ + {VALIGN_TOP, "top"}, + {VALIGN_CENTER, "center"}, + {VALIGN_BOTTOM, "bottom"}, +}) + +enum TextFit +{ + FIT_NONE, + FIT_SHRINK, + FIT_AUTO +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(TextFit, +{ + {FIT_NONE, "none"}, + {FIT_SHRINK, "shrink"}, + {FIT_AUTO, "auto"}, +}) // Call this before everything // Prints its messages @@ -21,6 +63,12 @@ void clean_sdl(); // Can return NULL TTF_Font* get_font(int size, const std::string& filename = default_font_name); +// Returns a rect to use during bliting of 2 surfaces +// base - surface on which the other is applied to +// applied - the surface which will be applied to the other +SDL_Rect surface_align(const SDL_Surface* base, const SDL_Surface* applied, + HorizontalAlign halign, VerticalAlign valign); + // Reads the file and tries to parse a JSON file with comments // cfg - output json struct // filename - filepath to open and read @@ -31,5 +79,11 @@ bool read_config_json(nlohmann::json& cfg, const std::string& filename, std::ost void json_extract(const nlohmann::json& j, const std::string& key, std::string& out); void json_extract(const nlohmann::json& j, const std::string& key, int& out); +void json_extract(const nlohmann::json& j, const std::string& key, bool& out); +void json_extract(const nlohmann::json& j, const std::string& key, HorizontalAlign& out); +void json_extract(const nlohmann::json& j, const std::string& key, VerticalAlign& out); +void json_extract(const nlohmann::json& j, const std::string& key, TextFit& out); +void json_extract(const nlohmann::json& j, const std::string& key, SDL_Color& out); +void json_extract(const nlohmann::json& j, const std::string& key, Uint8& out); #endif // SDL_HELPERS_H_