Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions tasmota/tasmota_support/support.ino
Original file line number Diff line number Diff line change
Expand Up @@ -2953,6 +2953,44 @@ String SettingsTextEscaped(uint32_t index) {
return HtmlEscape(SettingsText(index));
}

// Truncate src to max UTF-8 codepoints with optional max visual width.
// max_codepoints limits codepoints (0 = no limit)
// max_width limits *approximated* visual width (0 = no limit):
// narrow chars (1-2 byte: ASCII, Latin, Cyrillic) approximated to cost 1 width unit
// wide chars (3-4 byte: CJK, emoji) approximated to cost 2 width units
// Multi-byte characters are never split mid-sequence
// ZWJ sequences and skin tone modifiers are not recognized as single glyphs and may be split
String Utf8Truncate(const char *src, uint32_t max_codepoints, uint32_t max_width = 0) {
if (!max_codepoints && !max_width) {
return String(src);
}
size_t slen = strlen(src);
size_t bytes = 0;
size_t width = 0;
size_t chars = 0;
while (bytes < slen) {
if (max_codepoints && chars >= max_codepoints) { break; }
uint8_t lead = (uint8_t)src[bytes];
size_t clen = 1;
if (lead >= 0xF0) { clen = 4; }
else if (lead >= 0xE0) { clen = 3; }
else if (lead >= 0xC0) { clen = 2; }
size_t cwidth = (clen >= 3) ? 2 : 1;
if (max_width && (width + cwidth > max_width)) { break; }
if (bytes + clen > slen) { break; }
width += cwidth;
bytes += clen;
chars++;
}
if (!bytes) {
return String();
}
String result;
result.reserve(bytes);
result.concat(src, bytes);
return result;
}

String UrlEscape(const char *unescaped) {
static const char *hex = "0123456789ABCDEF";
String result;
Expand Down
57 changes: 29 additions & 28 deletions tasmota/tasmota_xdrv_driver/xdrv_01_9_webserver.ino
Original file line number Diff line number Diff line change
Expand Up @@ -1491,11 +1491,12 @@ void HandleRoot(void) {
uint32_t button_ptr = 0;
for (uint32_t button_idx = 1; button_idx <= TasmotaGlobal.devices_present; button_idx++) {
if (bitRead(Web.light_shutter_button_mask, button_idx -1)) { continue; } // Skip non-sequential light and/or shutter button
bool set_button = ((button_idx <= MAX_BUTTON_TEXT) && strlen(GetWebButton(button_idx -1)));
const char* web_button = (button_idx <= MAX_BUTTON_TEXT) ? GetWebButton(button_idx -1) : "";
bool has_web_button = (web_button[0] != '\0');
snprintf_P(stemp, sizeof(stemp), PSTR(" %d"), button_idx);
WSContentSend_P(HTTP_DEVICE_CONTROL, 100 / cols, button_idx, button_idx,
(set_button) ? HtmlEscape(GetWebButton(button_idx -1)).c_str() : (cols < 5) ? PSTR(D_BUTTON_TOGGLE) : "",
(set_button) ? "" : (TasmotaGlobal.devices_present > 1) ? stemp : "");
(has_web_button) ? HtmlEscape(web_button).c_str() : (cols < 5) ? PSTR(D_BUTTON_TOGGLE) : "",
(has_web_button) ? "" : (TasmotaGlobal.devices_present > 1) ? stemp : "");
button_ptr++;
if (0 == button_ptr % cols) { WSContentSend_P(PSTR("</tr><tr>")); }
}
Expand Down Expand Up @@ -1526,12 +1527,13 @@ void HandleRoot(void) {
if (1 == j) { break; } // Both buttons shown

shutter_button_idx--; // Right button is previous button (up)
bool set_button = ((shutter_button_idx <= MAX_BUTTON_TEXT) && strlen(GetWebButton(shutter_button_idx -1)));
const char* web_button = (shutter_button_idx <= MAX_BUTTON_TEXT) ? GetWebButton(shutter_button_idx -1) : "";
bool has_web_button = (web_button[0] != '\0');
snprintf_P(stemp, sizeof(stemp), PSTR("Shutter %d"), shutter_idx +1);
uint32_t shutter_real_to_percent_position = ShutterRealToPercentPosition(-9999, shutter_idx);
Web.shutter_slider[shutter_idx] = (shutter_options & 1) ? (100 - shutter_real_to_percent_position) : shutter_real_to_percent_position;
WSContentSend_P(HTTP_MSG_SLIDER_SHUTTER,
(set_button) ? HtmlEscape(GetWebButton(shutter_button_idx -1)).c_str() : stemp,
(has_web_button) ? HtmlEscape(web_button).c_str() : stemp,
shutter_idx +1,
Web.shutter_slider[shutter_idx],
shutter_idx +1);
Expand Down Expand Up @@ -1593,18 +1595,18 @@ void HandleRoot(void) {
Web.slider[2],
'n', 0); // n0 - Value id
WSContentSend_P(PSTR("</tr>"));
}
}

bool set_button = ((button_idx <= MAX_BUTTON_TEXT) && strlen(GetWebButton(button_idx -1)));
char first[2];
snprintf_P(first, sizeof(first), PSTR("%s"), PSTR(D_BUTTON_TOGGLE));
char butt_txt[4];
snprintf_P(butt_txt, sizeof(butt_txt), PSTR("%s"), (set_button) ? HtmlEscape(GetWebButton(button_idx -1)).c_str() : first);
const char* web_button = (button_idx <= MAX_BUTTON_TEXT) ? GetWebButton(button_idx -1) : "";
bool has_web_button = (web_button[0] != '\0');
// web_button non-empty: truncate to max 4 chars or "visual width" of 4 (e.g. "Ligh", "💡💡", "台灯"). Latin char = 1 width, CJK/emoji char = 2 width
// web_button empty: take first char of D_BUTTON_TOGGLE + button index (e.g. "T1", "П1", "开1")
String butt_text = (has_web_button) ? HtmlEscape(Utf8Truncate(web_button, 4, 4)) : Utf8Truncate(D_BUTTON_TOGGLE, 1);
char number[8];
WSContentSend_P(PSTR("<tr>"));
WSContentSend_P(HTTP_DEVICE_CONTROL, 15, button_idx, button_idx,
butt_txt,
(set_button) ? "" : itoa(button_idx, number, 10));
butt_text.c_str(),
(has_web_button) ? "" : itoa(button_idx, number, 10));
button_idx++;

Web.slider[3] = Settings->light_dimmer;
Expand All @@ -1627,15 +1629,15 @@ void HandleRoot(void) {
WSContentSend_P(PSTR("<tr>"));

if (button_idx < (light_device + light_devices)) {
bool set_button = ((button_idx <= MAX_BUTTON_TEXT) && strlen(GetWebButton(button_idx -1)));
char first[2];
snprintf_P(first, sizeof(first), PSTR("%s"), PSTR(D_BUTTON_TOGGLE));
char butt_txt[4];
snprintf_P(butt_txt, sizeof(butt_txt), PSTR("%s"), (set_button) ? HtmlEscape(GetWebButton(button_idx -1)).c_str() : first);
const char* web_button = (button_idx <= MAX_BUTTON_TEXT) ? GetWebButton(button_idx -1) : "";
bool has_web_button = (web_button[0] != '\0');
// web_button non-empty: truncate to max 4 chars or max "visual width" of 4 (e.g. "Ligh", "💡💡", "台灯"). Latin char = 1 width, CJK/emoji char = 2 width
// web_button empty: take first char of D_BUTTON_TOGGLE + button index (e.g. "T1", "П1", "开1")
String butt_text = (has_web_button) ? HtmlEscape(Utf8Truncate(web_button, 4, 4)) : Utf8Truncate(D_BUTTON_TOGGLE, 1);
char number[8];
WSContentSend_P(HTTP_DEVICE_CONTROL, 15, button_idx, button_idx,
butt_txt,
(set_button) ? "" : itoa(button_idx, number, 10));
butt_text.c_str(),
(has_web_button) ? "" : itoa(button_idx, number, 10));
button_idx++;
width = 85;
}
Expand All @@ -1654,17 +1656,16 @@ void HandleRoot(void) {
} else { // Settings->flag3.pwm_multi_channels - SetOption68 1 - Enable multi-channels PWM instead of Color PWM
stemp[0] = 'e'; stemp[1] = '0'; stemp[2] = '\0'; // e0
for (uint32_t i = 0; i < light_devices; i++) {
bool set_button = ((button_idx <= MAX_BUTTON_TEXT) && strlen(GetWebButton(button_idx -1)));
char first[2];
snprintf_P(first, sizeof(first), PSTR("%s"), PSTR(D_BUTTON_TOGGLE));
char butt_txt[4];
snprintf_P(butt_txt, sizeof(butt_txt), PSTR("%s"),
(set_button) ? HtmlEscape(GetWebButton(button_idx -1)).c_str() : first);
const char* web_button = (button_idx <= MAX_BUTTON_TEXT) ? GetWebButton(button_idx -1) : "";
bool has_web_button = (web_button[0] != '\0');
// web_button non-empty: truncate to max 4 chars or "visual width" of 4 (e.g. "Ligh", "💡💡", "台灯"). Latin char = 1 width, CJK/emoji char = 2 width
// web_button empty: take first char of D_BUTTON_TOGGLE + button index (e.g. "T1", "П1", "开1")
String butt_text = (has_web_button) ? HtmlEscape(Utf8Truncate(web_button, 4, 4)) : Utf8Truncate(D_BUTTON_TOGGLE, 1);
char number[8];
WSContentSend_P(PSTR("<tr>"));
WSContentSend_P(HTTP_DEVICE_CONTROL, 15, button_idx, button_idx,
butt_txt,
(set_button) ? "" : itoa(button_idx, number, 10));
butt_text.c_str(),
(has_web_button) ? "" : itoa(button_idx, number, 10));
button_idx++;

stemp[1]++; // e1 to e5 - Make unique ids
Expand Down