mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c4be8a34 | ||
|
|
fecb9eb3a4 | ||
|
|
53232eefab | ||
|
|
cc96e3c538 |
No files matched your search
@@ -7,13 +7,6 @@ on:
|
||||
jobs:
|
||||
build-release:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- pio_env: gh_release
|
||||
artifact_name: firmware-x4_x3
|
||||
- pio_env: sticky
|
||||
artifact_name: firmware-sticky
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -40,22 +33,15 @@ jobs:
|
||||
restore-keys: pio-${{ runner.os }}-
|
||||
|
||||
- name: Build CrossPoint
|
||||
env:
|
||||
CROSSPOINT_RELEASE_BUILD: "1"
|
||||
run: pio run -e ${{ matrix.pio_env }} -j1
|
||||
|
||||
- name: Name Release Firmware
|
||||
run: |
|
||||
cp ".pio/build/${{ matrix.pio_env }}/firmware.bin" \
|
||||
".pio/build/${{ matrix.pio_env }}/${{ matrix.artifact_name }}.bin"
|
||||
run: pio run -e gh_release
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
name: CrossPoint-${{ github.ref_name }}
|
||||
path: |
|
||||
.pio/build/${{ matrix.pio_env }}/bootloader.bin
|
||||
.pio/build/${{ matrix.pio_env }}/${{ matrix.artifact_name }}.bin
|
||||
.pio/build/${{ matrix.pio_env }}/firmware.elf
|
||||
.pio/build/${{ matrix.pio_env }}/firmware.map
|
||||
.pio/build/${{ matrix.pio_env }}/partitions.bin
|
||||
.pio/build/gh_release/bootloader.bin
|
||||
.pio/build/gh_release/firmware.bin
|
||||
.pio/build/gh_release/firmware.elf
|
||||
.pio/build/gh_release/firmware.map
|
||||
.pio/build/gh_release/partitions.bin
|
||||
@@ -7,13 +7,6 @@ jobs:
|
||||
build-release-candidate:
|
||||
if: startsWith(github.ref_name, 'release/')
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- pio_env: gh_release_rc
|
||||
artifact_name: firmware-x4_x3
|
||||
- pio_env: sticky
|
||||
artifact_name: firmware-sticky
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -39,27 +32,23 @@ jobs:
|
||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
||||
restore-keys: pio-${{ runner.os }}-
|
||||
|
||||
- name: Extract short SHA
|
||||
- name: Extract env
|
||||
run: |
|
||||
echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build CrossPoint Release Candidate
|
||||
env:
|
||||
CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }}
|
||||
run: pio run -e ${{ matrix.pio_env }} -j1
|
||||
|
||||
- name: Name Release Candidate Firmware
|
||||
run: |
|
||||
cp ".pio/build/${{ matrix.pio_env }}/firmware.bin" \
|
||||
".pio/build/${{ matrix.pio_env }}/${{ matrix.artifact_name }}-rc+${{ env.SHORT_SHA }}.bin"
|
||||
run: pio run -e gh_release_rc
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}-rc+${{ env.SHORT_SHA }}
|
||||
name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }}
|
||||
path: |
|
||||
.pio/build/${{ matrix.pio_env }}/bootloader.bin
|
||||
.pio/build/${{ matrix.pio_env }}/${{ matrix.artifact_name }}-rc+${{ env.SHORT_SHA }}.bin
|
||||
.pio/build/${{ matrix.pio_env }}/firmware.elf
|
||||
.pio/build/${{ matrix.pio_env }}/firmware.map
|
||||
.pio/build/${{ matrix.pio_env }}/partitions.bin
|
||||
.pio/build/gh_release_rc/bootloader.bin
|
||||
.pio/build/gh_release_rc/firmware.bin
|
||||
.pio/build/gh_release_rc/firmware.elf
|
||||
.pio/build/gh_release_rc/firmware.map
|
||||
.pio/build/gh_release_rc/partitions.bin
|
||||
+74
-19
@@ -99,8 +99,11 @@ void SdCardFont::freeStyleMiniData(PerStyle& s) {
|
||||
s.miniIntervals = nullptr;
|
||||
delete[] s.miniGlyphs;
|
||||
s.miniGlyphs = nullptr;
|
||||
delete[] s.miniBitmap;
|
||||
s.miniBitmap = nullptr;
|
||||
for (auto& chunk : s.miniBitmapChunks) {
|
||||
delete[] chunk;
|
||||
chunk = nullptr;
|
||||
}
|
||||
s.miniBitmapChunkCount = 0;
|
||||
s.miniIntervalCount = 0;
|
||||
s.miniGlyphCount = 0;
|
||||
s.miniIntervalCapacity = 0;
|
||||
@@ -1010,30 +1013,66 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
totalBitmapSize += s.miniGlyphs[i].dataLength;
|
||||
}
|
||||
|
||||
if (!ensureArrayCapacity(s.miniBitmap, s.miniBitmapCapacity, totalBitmapSize)) {
|
||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap (%u bytes) for style %u", totalBitmapSize, styleIdx);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
s.miniBitmapUsed = totalBitmapSize; // underuse-hysteresis signal for resetStyleMiniData
|
||||
|
||||
// Read bitmap data sorted by file offset
|
||||
std::sort(readOrder, readOrder + validCount,
|
||||
[&](uint32_t a, uint32_t b) { return s.miniGlyphs[a].dataOffset < s.miniGlyphs[b].dataOffset; });
|
||||
|
||||
uint32_t miniBitmapOffset = 0;
|
||||
// Assemble the bitmap arena from on-demand 4 KB chunks (see MINI_BM_CHUNK_*).
|
||||
// `span` is the running virtual offset into the arena; each glyph is placed
|
||||
// wholly within one chunk, skipping past a boundary rather than straddling it.
|
||||
// glyph.dataOffset becomes the virtual offset, decoded at render time by
|
||||
// miniGlyphBitmap(). Chunks are allocated only as span reaches them, so a
|
||||
// sparse page never allocates the full ceiling.
|
||||
uint32_t span = 0;
|
||||
uint32_t lastBitmapEnd = UINT32_MAX;
|
||||
for (uint32_t i = 0; i < validCount; i++) {
|
||||
uint32_t mapIdx = readOrder[i];
|
||||
EpdGlyph& glyph = s.miniGlyphs[mapIdx];
|
||||
const uint32_t len = glyph.dataLength;
|
||||
|
||||
if (glyph.dataLength == 0) {
|
||||
glyph.dataOffset = miniBitmapOffset;
|
||||
if (len == 0) {
|
||||
glyph.dataOffset = span;
|
||||
continue;
|
||||
}
|
||||
if (len > MINI_BM_CHUNK_SIZE) {
|
||||
// A single glyph larger than one chunk cannot be placed. Real fonts never
|
||||
// hit this (a 4 KB 2bpp glyph would be enormous); bail rather than corrupt.
|
||||
LOG_ERR("SDCF", "Prewarm: glyph %u B exceeds chunk %u B (style %u)", len, MINI_BM_CHUNK_SIZE, styleIdx);
|
||||
file.close();
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
|
||||
// Skip to the next chunk boundary if this glyph would straddle one.
|
||||
const uint32_t within = span & (MINI_BM_CHUNK_SIZE - 1);
|
||||
if (within != 0 && within + len > MINI_BM_CHUNK_SIZE) {
|
||||
span += MINI_BM_CHUNK_SIZE - within;
|
||||
}
|
||||
const uint32_t chunkIdx = span >> MINI_BM_CHUNK_SHIFT;
|
||||
if (chunkIdx >= MINI_BM_MAX_CHUNKS) {
|
||||
LOG_ERR("SDCF", "Prewarm: mini bitmap needs > %u chunks (style %u)", MINI_BM_MAX_CHUNKS, styleIdx);
|
||||
file.close();
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
if (!s.miniBitmapChunks[chunkIdx]) {
|
||||
s.miniBitmapChunks[chunkIdx] = new (std::nothrow) uint8_t[MINI_BM_CHUNK_SIZE];
|
||||
if (!s.miniBitmapChunks[chunkIdx]) {
|
||||
LOG_ERR("SDCF", "Failed to allocate mini bitmap chunk %u (style %u)", chunkIdx, styleIdx);
|
||||
file.close();
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
if (chunkIdx + 1 > s.miniBitmapChunkCount) s.miniBitmapChunkCount = chunkIdx + 1;
|
||||
}
|
||||
|
||||
const uint32_t off = span & (MINI_BM_CHUNK_SIZE - 1);
|
||||
uint32_t fileOff = s.bitmapFileOffset + glyph.dataOffset;
|
||||
if (fileOff != lastBitmapEnd) {
|
||||
if (!file.seekSet(fileOff)) {
|
||||
@@ -1046,18 +1085,21 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
}
|
||||
seekCount++;
|
||||
}
|
||||
if (file.read(s.miniBitmap + miniBitmapOffset, glyph.dataLength) != static_cast<int>(glyph.dataLength)) {
|
||||
if (file.read(s.miniBitmapChunks[chunkIdx] + off, len) != static_cast<int>(len)) {
|
||||
LOG_ERR("SDCF", "Prewarm: short bitmap read (style %u)", styleIdx);
|
||||
delete[] readOrder;
|
||||
delete[] mappings;
|
||||
freeStyleMiniData(s);
|
||||
return static_cast<int>(cpCount);
|
||||
}
|
||||
lastBitmapEnd = fileOff + glyph.dataLength;
|
||||
lastBitmapEnd = fileOff + len;
|
||||
|
||||
glyph.dataOffset = miniBitmapOffset;
|
||||
miniBitmapOffset += glyph.dataLength;
|
||||
glyph.dataOffset = span;
|
||||
span += len;
|
||||
}
|
||||
// Footprint including inter-chunk padding: the underuse-hysteresis signal.
|
||||
s.miniBitmapUsed = span;
|
||||
s.miniBitmapCapacity = s.miniBitmapChunkCount * MINI_BM_CHUNK_SIZE;
|
||||
}
|
||||
|
||||
uint32_t sdTime = millis() - sdStart;
|
||||
@@ -1080,7 +1122,10 @@ int SdCardFont::prewarmStyle(uint8_t styleIdx, const uint32_t* codepoints, uint3
|
||||
s.miniMetadataOnly = metadataOnly;
|
||||
s.miniHysteresisPending = !metadataOnly; // one hysteresis evaluation per rebuild
|
||||
memset(&s.miniData, 0, sizeof(s.miniData));
|
||||
s.miniData.bitmap = s.miniBitmap;
|
||||
// The mini bitmap is chunked (non-contiguous), so there is no single base
|
||||
// pointer. Prewarmed SD glyphs are resolved via SdCardFont::miniGlyphBitmap()
|
||||
// in the renderer; leave bitmap null so any stray &bitmap[dataOffset] faults.
|
||||
s.miniData.bitmap = nullptr;
|
||||
s.miniData.glyph = s.miniGlyphs;
|
||||
s.miniData.intervals = s.miniIntervals;
|
||||
s.miniData.intervalCount = s.miniIntervalCount;
|
||||
@@ -1517,4 +1562,14 @@ const uint8_t* SdCardFont::getOverflowBitmap(const EpdGlyph* glyph) const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint8_t* SdCardFont::miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const {
|
||||
const auto* octx = static_cast<const OverflowContext*>(ctx);
|
||||
const PerStyle& s = styles_[octx->styleIdx];
|
||||
const uint32_t chunkIdx = dataOffset >> MINI_BM_CHUNK_SHIFT;
|
||||
if (chunkIdx >= s.miniBitmapChunkCount) return nullptr;
|
||||
const uint8_t* chunk = s.miniBitmapChunks[chunkIdx];
|
||||
if (!chunk) return nullptr;
|
||||
return chunk + (dataOffset & (MINI_BM_CHUNK_SIZE - 1));
|
||||
}
|
||||
|
||||
SdCardFont* SdCardFont::fromMissCtx(void* ctx) { return static_cast<OverflowContext*>(ctx)->self; }
|
||||
@@ -94,6 +94,12 @@ class SdCardFont {
|
||||
// Returns the bitmap for an on-demand-loaded (overflow) glyph.
|
||||
const uint8_t* getOverflowBitmap(const EpdGlyph* glyph) const;
|
||||
|
||||
// Resolves a prewarmed mini glyph's chunked bitmap. `ctx` is the glyphMissCtx
|
||||
// (an OverflowContext identifying the style); `dataOffset` is the glyph's
|
||||
// virtual offset into the style's chunked arena. Returns nullptr if the chunk
|
||||
// is absent or out of range. Called by GfxRenderer::getGlyphBitmap().
|
||||
const uint8_t* miniGlyphBitmap(const void* ctx, uint32_t dataOffset) const;
|
||||
|
||||
// Extract SdCardFont* from an opaque glyphMissCtx pointer.
|
||||
// Used by GfxRenderer::getGlyphBitmap() to recover the SdCardFont from EpdFontData::glyphMissCtx.
|
||||
static SdCardFont* fromMissCtx(void* ctx);
|
||||
@@ -129,6 +135,18 @@ class SdCardFont {
|
||||
uint8_t ligaturePairCount = 0;
|
||||
};
|
||||
|
||||
// The per-style mini bitmap arena is stored as a list of fixed-size chunks
|
||||
// rather than one contiguous block. A whole page's 2bpp glyph bitmaps can run
|
||||
// tens of KB; on a fragmented heap a single contiguous allocation of that size
|
||||
// fails even when the same bytes are available as several smaller free blocks.
|
||||
// Chunking lets the arena be assembled from blocks the allocator can actually
|
||||
// provide. Each glyph's bitmap is placed wholly within one chunk (never
|
||||
// straddling a boundary), so a glyph is addressed by a virtual offset that
|
||||
// maps to (chunk index, offset-in-chunk) via miniGlyphBitmap().
|
||||
static constexpr uint32_t MINI_BM_CHUNK_SHIFT = 12; // 4 KB chunks
|
||||
static constexpr uint32_t MINI_BM_CHUNK_SIZE = 1u << MINI_BM_CHUNK_SHIFT;
|
||||
static constexpr uint32_t MINI_BM_MAX_CHUNKS = 24; // 96 KB ceiling per style/page
|
||||
|
||||
// All per-style data: file offsets, intervals, kern/lig, prewarm cache, EpdFont
|
||||
struct PerStyle {
|
||||
CpFontHeader header{};
|
||||
@@ -188,7 +206,10 @@ class SdCardFont {
|
||||
EpdFontData miniData{};
|
||||
EpdUnicodeInterval* miniIntervals = nullptr;
|
||||
EpdGlyph* miniGlyphs = nullptr;
|
||||
uint8_t* miniBitmap = nullptr;
|
||||
// Chunked mini bitmap arena (see MINI_BM_CHUNK_* above). Chunks are allocated
|
||||
// on demand during prewarm; miniBitmapChunkCount is how many are live.
|
||||
uint8_t* miniBitmapChunks[MINI_BM_MAX_CHUNKS] = {};
|
||||
uint32_t miniBitmapChunkCount = 0;
|
||||
uint32_t miniIntervalCount = 0;
|
||||
uint32_t miniGlyphCount = 0;
|
||||
uint32_t miniIntervalCapacity = 0;
|
||||
|
||||
@@ -81,6 +81,9 @@ const uint8_t* GfxRenderer::getGlyphBitmap(const EpdFontData* fontData, const Ep
|
||||
if (sdFont->isOverflowGlyph(glyph)) {
|
||||
return sdFont->getOverflowBitmap(glyph); // may be nullptr for zero-width glyphs
|
||||
}
|
||||
// Prewarmed SD glyph: the mini bitmap is chunked (non-contiguous), so there is
|
||||
// no fontData->bitmap base to index. Resolve via the per-style chunk table.
|
||||
return sdFont->miniGlyphBitmap(fontData->glyphMissCtx, glyph->dataOffset);
|
||||
}
|
||||
return &fontData->bitmap[glyph->dataOffset];
|
||||
}
|
||||
|
||||
@@ -148,7 +148,8 @@ std::string getPanicInfo(bool full) {
|
||||
|
||||
bool isRebootFromPanic() {
|
||||
const auto resetReason = esp_reset_reason();
|
||||
return resetReason == ESP_RST_PANIC || resetReason == ESP_RST_CPU_LOCKUP;
|
||||
return resetReason == ESP_RST_PANIC || resetReason == ESP_RST_CPU_LOCKUP || resetReason == ESP_RST_INT_WDT ||
|
||||
resetReason == ESP_RST_TASK_WDT || resetReason == ESP_RST_WDT;
|
||||
}
|
||||
|
||||
} // namespace HalSystem
|
||||
+2
-1
@@ -200,6 +200,7 @@ board_build.mcu = esp32s3
|
||||
build_flags =
|
||||
${base.build_flags}
|
||||
-DFREEINK_DEVICE_STICKY=1
|
||||
; CROSSPOINT_VERSION is set by scripts/git_branch.py.
|
||||
; git_branch.py only injects CROSSPOINT_VERSION for the default env; set it here.
|
||||
-DCROSSPOINT_VERSION=\"${crosspoint.version}-sticky\"
|
||||
-DENABLE_SERIAL_LOG
|
||||
-DLOG_LEVEL=2
|
||||
+10
-27
@@ -1,20 +1,9 @@
|
||||
"""
|
||||
PlatformIO pre-build script: set the embedded CROSSPOINT_VERSION.
|
||||
PlatformIO pre-build script: inject git branch and short SHA into
|
||||
CROSSPOINT_VERSION for the default (dev) environment.
|
||||
|
||||
Version schema by device and build type:
|
||||
|
||||
X3/X4 development: <base>-dev-<branch>-<git-sha>
|
||||
X3/X4 production: <base>
|
||||
X3/X4 RC: <base>-rc+<git-sha>
|
||||
|
||||
Sticky development: <base>-dev-<branch>-<git-sha>
|
||||
Sticky production: <base>
|
||||
Sticky RC: <base>-rc+<git-sha>
|
||||
|
||||
This script injects the X3/X4 development and all Sticky versions. The
|
||||
dedicated X3/X4 production and RC environments set their versions in
|
||||
platformio.ini. CI marks production builds with CROSSPOINT_RELEASE_BUILD and
|
||||
supplies the seven-character RC SHA through CROSSPOINT_RC_HASH.
|
||||
Results in a version string like: 1.1.0-dev-feat-kosync-xpath-05c6cf8
|
||||
Release environments are unaffected; they set CROSSPOINT_VERSION in the ini.
|
||||
"""
|
||||
|
||||
import configparser
|
||||
@@ -88,22 +77,16 @@ def get_base_version(project_dir):
|
||||
|
||||
|
||||
def inject_version(env):
|
||||
pioenv = env['PIOENV']
|
||||
if pioenv not in ('default', 'sticky'):
|
||||
# Only applies to the dev (default) environment; release envs set the
|
||||
# version via build_flags in platformio.ini and are unaffected.
|
||||
if env['PIOENV'] != 'default':
|
||||
return
|
||||
|
||||
project_dir = env['PROJECT_DIR']
|
||||
base_version = get_base_version(project_dir)
|
||||
|
||||
rc_hash = os.environ.get('CROSSPOINT_RC_HASH')
|
||||
if pioenv == 'sticky' and rc_hash:
|
||||
version_string = f'{base_version}-rc+{rc_hash}'
|
||||
elif pioenv == 'sticky' and os.environ.get('CROSSPOINT_RELEASE_BUILD'):
|
||||
version_string = base_version
|
||||
else:
|
||||
branch = get_git_branch(project_dir)
|
||||
short_sha = get_git_short_sha(project_dir)
|
||||
version_string = f'{base_version}-dev-{branch}-{short_sha}'
|
||||
branch = get_git_branch(project_dir)
|
||||
short_sha = get_git_short_sha(project_dir)
|
||||
version_string = f'{base_version}-dev-{branch}-{short_sha}'
|
||||
|
||||
env.Append(CPPDEFINES=[('CROSSPOINT_VERSION', f'\\"{version_string}\\"')])
|
||||
print(f'CrossPoint build version: {version_string}')
|
||||
|
||||
@@ -772,7 +772,9 @@ void WifiSelectionActivity::render(RenderLock&&) {
|
||||
Rect screen = theme.getScreenSafeArea(renderer, true, false);
|
||||
|
||||
// Draw header
|
||||
char countStr[32];
|
||||
// STR_NETWORKS_FOUND is ~37 bytes once the Arabic translation is substituted,
|
||||
// so 32 truncated it. See ClockSyncActivity for the same class of bug.
|
||||
char countStr[64];
|
||||
snprintf(countStr, sizeof(countStr), tr(STR_NETWORKS_FOUND), realNetworkCount);
|
||||
GUI.drawHeader(renderer, Rect{screen.x, screen.y + metrics.topPadding, screen.width, metrics.headerHeight},
|
||||
tr(STR_WIFI_NETWORKS), countStr);
|
||||
|
||||
@@ -334,7 +334,9 @@ void ClockOffsetActivity::render(RenderLock&&) {
|
||||
char timeBuf[9];
|
||||
const uint8_t encoded = encodeOffset(sign, hours, minutesQuarter);
|
||||
if (halClock.formatTime(timeBuf, sizeof(timeBuf), encoded, SETTINGS.clockFormat == 1)) {
|
||||
char preview[24];
|
||||
// 24 bytes did not even hold the label itself once translated: STR_CURRENT_TIME
|
||||
// is 26 bytes in Russian, 24 in Arabic and Ukrainian. See ClockSyncActivity.
|
||||
char preview[64];
|
||||
snprintf(preview, sizeof(preview), "%s %s", tr(STR_CURRENT_TIME), timeBuf);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, centreY + 60, preview);
|
||||
}
|
||||
|
||||
@@ -118,7 +118,10 @@ void ClockSyncActivity::render(RenderLock&&) {
|
||||
case SUCCESS: {
|
||||
renderer.drawCenteredText(UI_12_FONT_ID, midY - 20, tr(STR_CLOCK_SYNC_OK), true, EpdFontFamily::BOLD);
|
||||
if (syncedTime[0] != '\0') {
|
||||
char line[32];
|
||||
// Sized for the label in any language: STR_CURRENT_TIME is 26 bytes in
|
||||
// Russian (UTF-8 Cyrillic is 2 bytes per letter) versus 13 in English,
|
||||
// plus a separator and up to "08:56 PM".
|
||||
char line[64];
|
||||
snprintf(line, sizeof(line), "%s %s", tr(STR_CURRENT_TIME), syncedTime);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, midY + 10, line);
|
||||
}
|
||||
|
||||
+20
-2
@@ -385,10 +385,28 @@ bool Dictionary::readDefinition(const DictLocation& location, std::string& out,
|
||||
|
||||
std::string Dictionary::cleanWord(const char* word) {
|
||||
if (!word) return "";
|
||||
const auto* b = reinterpret_cast<const unsigned char*>(word);
|
||||
size_t start = 0;
|
||||
size_t end = strlen(word);
|
||||
while (start < end && !isWordByte(static_cast<unsigned char>(word[start]))) start++;
|
||||
while (end > start && !isWordByte(static_cast<unsigned char>(word[end - 1]))) end--;
|
||||
// Curly quotes and dashes (General Punctuation U+2000-U+206F = E2 80/81 xx)
|
||||
// are all >= 0x80, so isWordByte keeps them; strip those 3-byte codepoints
|
||||
// from the edges too, or EPUB text like garage.” never matches a headword.
|
||||
while (start < end) {
|
||||
if (!isWordByte(b[start]))
|
||||
start++;
|
||||
else if (end - start >= 3 && b[start] == 0xE2 && (b[start + 1] == 0x80 || b[start + 1] == 0x81))
|
||||
start += 3;
|
||||
else
|
||||
break;
|
||||
}
|
||||
while (end > start) {
|
||||
if (!isWordByte(b[end - 1]))
|
||||
end--;
|
||||
else if (end - start >= 3 && b[end - 3] == 0xE2 && (b[end - 2] == 0x80 || b[end - 2] == 0x81))
|
||||
end -= 3;
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (start >= end) return "";
|
||||
|
||||
std::string result(word + start, end - start);
|
||||
|
||||
Reference in new issue
Block a user