Centering text when using multiple printf

Hi,
I am having difficulties centering text consisting of two printf next to each other.

I would like to center the icon below the headline and its corresponding temperature in the middle of the screen. I am unsure how to do that though. Is there a way to calculate how wide the text is going to be so I can dynamically set an offset?

This is the code drawing the relevant part of the screen:

int weather_pos = 40;
int weather_current_offset = 140;

it.printf(412, weather_pos, id(font_header_book), color_white, TextAlign::TOP_CENTER, "Wetter");

it.printf(40, weather_pos + weather_current_offset + 10, id(font_mdi_large), color_white, TextAlign::TOP_LEFT, "%s", icon_map[id(current_weather_condition).state.c_str()].c_str());
   
it.printf(785, weather_pos + weather_current_offset, id(font_large_bold), color_white, TextAlign::TOP_RIGHT, "%2.0f°C", id(current_weather_temperature).state);

Best,
Paul

1 Like

Is there really no way to do this?

it.get_text_ bounds(…) can be used to get the width but you would need to create a temp string first with the actual text using sprintf(…) or vsnprintf(…)

get_text_bounds (int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width, int *height)

As an uglier alternative solution, perhaps you could get away with a few more hardcoded if scenario spacings based on the presence/absence of a negative sign (< 0) and if the temp has more than 1 digit (less than -9 or more than 9).

It’s looking slick so fair enough if you want to get it pixel perfect though.

Thank you for the hints sadly I am not a very good C programmer can you give me some further hints? I tried:

// Helper
int* calculated_width;
int* calculated_height;
int* calculated_x;
int* calculated_y;

const char tmp_icon_string = sprintf("%s", icon_map[id(current_weather_condition).state.c_str()].c_str());

it.get_text_bounds(40, weather_pos + weather_current_offset + 10, tmp_icon_string, id(font_mdi_large), TextAlign::TOP_LEFT, calculated_x, calculated_y, calculated_width, calculated_height);

it.printf(*calculated_x, *calculated_y, id(font_mdi_large), color_white, TextAlign::TOP_LEFT, "%s", icon_map[id(current_weather_condition).state.c_str()].c_str());

This fails with:

/config/esphome/epaper-frame-wohnzimmer.yaml:535:110: warning: ISO C++ forbids converting a string constant to 'char*' [-Wwrite-strings]
         const char tmp_icon_string = sprintf("%s", icon_map[id(current_weather_condition).state.c_str()].c_str());
                                                                                                              ^
/config/esphome/epaper-frame-wohnzimmer.yaml:537:192: error: invalid conversion from 'char' to 'const char*' [-fpermissive]

Thanks! This is also a good idea if I can’t get the it.get_text_bounds to work.

1 Like

Soo this seems to work, it doesn’t feel very elegant though.
If anyone has suggestions on how to improve it, I would be very happy!
For example do I need to store calculated_x, calculated_y, calculated_height even though I never use it?

// Helper
int calculated_icon_width;
int calculated_temperature_width;
int calculated_height;
int calculated_x;
int calculated_y;
int padding = 0;
int screen_width = 825;
      
char icon_string[80];
sprintf(icon_string, "%s", icon_map[id(current_weather_condition).state.c_str()].c_str());
it.get_text_bounds(0, 0, icon_string, id(font_mdi_large), TextAlign::TOP_LEFT, &calculated_x, &calculated_y, &calculated_icon_width, &calculated_height);

char temperature_string[80];
sprintf(temperature_string, "%2.0f°C", id(current_weather_temperature).state);
it.get_text_bounds(0, 0, temperature_string, id(font_large_bold), TextAlign::TOP_LEFT, &calculated_x, &calculated_y, &calculated_temperature_width, &calculated_height);

int left_padding = (screen_width - (calculated_icon_width + calculated_temperature_width + padding)) / 2;

it.printf(left_padding, weather_pos + weather_current_offset + 8, id(font_mdi_large), color_white, TextAlign::TOP_LEFT, "%s", icon_map[id(current_weather_condition).state.c_str()].c_str());
it.printf(left_padding + padding + calculated_icon_width, weather_pos + weather_current_offset, id(font_large_bold), color_white, TextAlign::TOP_LEFT, "%2.0f°C", id(current_weather_temperature).state);

Looks like you got it working while I was drafting reply. Nice work.

// Need to declare regular int in your code.  The address of operator & will make them int *
//for the get_text_bounds() function
int calculated_width;
int calculated_height;
int calculated_x;
int calculated_y;

//c_str is an array of char   
char buffer[32];
vsnprintf(buffer, sizeof(buffer), "%s", icon_map[id(current_weather_condition).state.c_str()].c_str()  );

//calculate the width of the icon text, location isn't important 
it.get_text_bounds(0, 0, buffer, id(font_mdi_large), TextAlign::TOP_LEFT, &calculated_x, &calculated_y, &calculated_width, &calculated_height);

//print icon text offset from center of screen on left side
it.printf(412-calculated_width-10, weather_current_offset + 10, id(font_mdi_large), color_white, TextAlign::TOP_LEFT, "%s", icon_map[id(current_weather_condition).state.c_str()].c_str());

Thanks a lot for your help!

Can’t you just align that with TextAlign::Right and add the offset you want in from the right? It should then self centre from the right boundary, shouldn’t it. Perhaps that doesn’t play nice when you it moves from single to double digit.

// Aligned on right edge
      it.print(it.get_width(), 0, id(my_font), TextAlign::TOP_RIGHT, "Right aligned");

esphome drawing static text

Not sure I follow. Aligning the text to the icon will make the distance correct but won’t center the two elements as the icons needs centering as well.

Whoops I meant the text element. Mobile copying.

What I mean is.

Text align left the Icon and offset it in from the left edge to where you want it.

Then text align right the degrees and degree sign in from the right, with the appropriate offset from the right side.

That should right justify the text element, so that when it moves to double digits the number fills in from the right(backwards or R to L) from the right side offset, if that makes sense.

But then it won’t be centered then. I want to center both the icon and the text on screen giving the icon a fixed position won’t do that.

1 Like

Hi,

I thought I would post my solution here for others facing this:

  • Create a header file (“e.g. text_utils.h”) which you include in the esp yaml file.
  • In this header, define several overloads of the same function to calculate the width given different types: i.e. string, int, float, and time.
  • Then, in all lambda calls, invoke GetTextWidth without worrying about the type and it will be resolved accordingly
int GetTextBounds(esphome::display::Display* it, esphome::font::Font *font, const char *buffer)
{
    int x1 = 0;     // A pointer to store the returned x coordinate of the upper left corner in. 
    int y1 = 0;     // A pointer to store the returned y coordinate of the upper left corner in.
    int width = 0;  // A pointer to store the returned text width in.
    int height = 0; // A pointer to store the returned text height in. 
    it->get_text_bounds(0, 0, buffer, font, TextAlign::TOP_LEFT, &x1, &y1, &width, &height);
    return width;
}

int GetTextWidth(esphome::display::Display* it, esphome::font::Font *font, const char* formatting, const char *raw_text){
    char temp_buffer[80];
    sprintf(temp_buffer, formatting, raw_text);
    return GetTextBounds(it, font, temp_buffer);
}
...  // overloads for int and float
// Calculate the width of time format
int GetTextWidth(esphome::display::Display* it, esphome::font::Font *font, const char* formatting, esphome::ESPTime time){
    auto c_tm = time.to_c_tm();
    size_t buffer_length = 80;
    char temp_buffer[buffer_length];
    strftime(temp_buffer, buffer_length, formatting, &c_tm);
    return GetTextBounds(it, font, temp_buffer);
}

Using it within lambda is quite trivial:

      // Estimating total length of text
      int day_size = GetTextWidth(&it, id(day_font), "%d", id(hass_time).now());
      int icon_size = GetTextWidth(&it, id(font_mdi_large), "%s", weather_icon_map[id(condition_now).state.c_str()].c_str());
      int temperature_size = GetTextWidth(&it, id(temperature_font), "%s°C", id(temperature_now).state.c_str());
5 Likes

Great, exactly what I am looking for.

Do you mind sharing how the overload funtions for int and floats look like?

Hi, the overloads for float and int are here:

int GetTextWidth(esphome::display::Display* it, esphome::font::Font *font, const char* formatting, const int raw_int){
    char temp_buffer[80];
    sprintf(temp_buffer, formatting, raw_int);
    return GetTextBounds(it, font, temp_buffer);
}

int GetTextWidth(esphome::display::Display* it, esphome::font::Font *font, const char* formatting, const float raw_float){
    char temp_buffer[80];
    sprintf(temp_buffer, formatting, raw_float);
    return GetTextBounds(it, font, temp_buffer);
}
3 Likes

@jambuu
Today I had the issue that the overloads gave some errors.
I changed things like esphome::font::Font , esphome::display::Display* and esphome::ESPTime and got my overloads working again.

I wanted to update this topic with my findings, but surprisingly found out that you already changed your examples just today! What a coincidence! :slight_smile:
(and thanks again for sharing)

Hi @jjansen85, totally coincidence, I had postpone the update of home assistant due to lack of time and got some time to look into the problem, great that you also fixed it.

I realize I am definitely “late to the party”, but I thought I would throw in a slightly more efficient version that directly uses a Font method (which is what the Display method does on the second line of its code):

// Get the width (in pixels) of a char string using a given Font
int get_text_width(esphome::font::Font *font, const char *text)
{
    int width, x_offset, baseline, height;
    
    font->measure(text, &width, &x_offset, &baseline, &height);
    return width;
}

Specifying a TextAlign value does not affect the actual width value returned (only the x and/or y values which are not returned in either implementation here).