170 lines
6.0 KiB
C++
170 lines
6.0 KiB
C++
#pragma once
|
||
#include <HalStorage.h>
|
||
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include "FootnoteEntry.h"
|
||
#include "blocks/ImageBlock.h"
|
||
#include "blocks/TextBlock.h"
|
||
|
||
static constexpr uint8_t MAX_TABLE_COLS = 8;
|
||
static constexpr uint16_t MAX_TABLE_ROWS = 48;
|
||
static constexpr uint8_t TABLE_CELL_PADDING = 5;
|
||
static constexpr uint16_t MIN_COL_INNER_WIDTH = 24;
|
||
static constexpr uint8_t MAX_CELL_LINES = 64;
|
||
|
||
enum PageElementTag : uint8_t {
|
||
TAG_PageLine = 1,
|
||
TAG_PageImage = 2,
|
||
TAG_PageTable = 3,
|
||
TAG_PageHR = 4,
|
||
};
|
||
|
||
// represents something that has been added to a page
|
||
class PageElement {
|
||
public:
|
||
int16_t xPos;
|
||
int16_t yPos;
|
||
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
||
virtual ~PageElement() = default;
|
||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||
virtual bool serialize(FsFile& file) = 0;
|
||
virtual PageElementTag getTag() const = 0; // Add type identification
|
||
};
|
||
|
||
// a line from a block element
|
||
class PageLine final : public PageElement {
|
||
std::shared_ptr<TextBlock> block;
|
||
|
||
public:
|
||
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||
const std::shared_ptr<TextBlock>& getBlock() const { return block; }
|
||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||
bool serialize(FsFile& file) override;
|
||
PageElementTag getTag() const override { return TAG_PageLine; }
|
||
static std::unique_ptr<PageLine> deserialize(FsFile& file);
|
||
};
|
||
|
||
// New PageImage class
|
||
class PageImage final : public PageElement {
|
||
std::shared_ptr<ImageBlock> imageBlock;
|
||
|
||
public:
|
||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||
bool serialize(FsFile& file) override;
|
||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||
const ImageBlock& getImageBlock() const { return *imageBlock; }
|
||
};
|
||
|
||
class PageHR final : public PageElement {
|
||
int16_t width;
|
||
|
||
public:
|
||
PageHR(const int16_t xPos, const int16_t yPos, const int16_t width) : PageElement(xPos, yPos), width(width) {}
|
||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||
bool serialize(FsFile& file) override;
|
||
PageElementTag getTag() const override { return TAG_PageHR; }
|
||
static std::unique_ptr<PageHR> deserialize(FsFile& file);
|
||
};
|
||
|
||
struct TableCell {
|
||
std::vector<std::shared_ptr<TextBlock>> lines;
|
||
bool isHeader = false;
|
||
};
|
||
|
||
struct TableRow {
|
||
std::vector<TableCell> cells;
|
||
uint16_t height = 0; // pixel height including 2×CELL_PADDING
|
||
bool isHeaderRow = false; // drives 2px separator below this row
|
||
};
|
||
|
||
class PageTableFragment final : public PageElement {
|
||
uint8_t columnCount = 0;
|
||
uint16_t totalWidth = 0;
|
||
uint16_t totalHeight = 0;
|
||
std::array<uint16_t, MAX_TABLE_COLS> colWidths = {};
|
||
std::vector<TableRow> rows;
|
||
|
||
public:
|
||
PageTableFragment(uint8_t colCount, uint16_t totalWidth, uint16_t totalHeight,
|
||
std::array<uint16_t, MAX_TABLE_COLS> colWidths, std::vector<TableRow> rows, int16_t xPos,
|
||
int16_t yPos)
|
||
: PageElement(xPos, yPos),
|
||
columnCount(colCount),
|
||
totalWidth(totalWidth),
|
||
totalHeight(totalHeight),
|
||
colWidths(colWidths),
|
||
rows(std::move(rows)) {}
|
||
|
||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||
bool serialize(FsFile& file) override;
|
||
static std::unique_ptr<PageTableFragment> deserialize(FsFile& file);
|
||
PageElementTag getTag() const override { return TAG_PageTable; }
|
||
uint16_t getTotalHeight() const { return totalHeight; }
|
||
};
|
||
|
||
class Page {
|
||
public:
|
||
// the list of block index and line numbers on this page
|
||
std::vector<std::shared_ptr<PageElement>> elements;
|
||
std::vector<FootnoteEntry> footnotes;
|
||
static constexpr uint16_t MAX_FOOTNOTES_PER_PAGE = 16;
|
||
|
||
void addFootnote(const char* number, const char* href) {
|
||
if (footnotes.size() >= MAX_FOOTNOTES_PER_PAGE) return; // Cap per-page footnotes
|
||
FootnoteEntry entry;
|
||
strncpy(entry.number, number, sizeof(entry.number) - 1);
|
||
entry.number[sizeof(entry.number) - 1] = '\0';
|
||
strncpy(entry.href, href, sizeof(entry.href) - 1);
|
||
entry.href[sizeof(entry.href) - 1] = '\0';
|
||
footnotes.push_back(entry);
|
||
}
|
||
|
||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||
void renderTextOnly(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||
bool serialize(FsFile& file) const;
|
||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||
|
||
// Check if page contains any images (used to force full refresh)
|
||
bool hasImages() const {
|
||
return std::any_of(elements.begin(), elements.end(),
|
||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||
}
|
||
|
||
// Get bounding box of all images on the page (union of image rects)
|
||
// Returns false if no images. Coordinates are relative to page origin.
|
||
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
||
bool found = false;
|
||
int16_t minX = INT16_MAX, minY = INT16_MAX, maxX = INT16_MIN, maxY = INT16_MIN;
|
||
for (const auto& el : elements) {
|
||
if (el->getTag() == TAG_PageImage) {
|
||
const auto& img = static_cast<const PageImage&>(*el);
|
||
int16_t x = img.xPos;
|
||
int16_t y = img.yPos;
|
||
int16_t right = x + img.getImageBlock().getWidth();
|
||
int16_t bottom = y + img.getImageBlock().getHeight();
|
||
minX = std::min(minX, x);
|
||
minY = std::min(minY, y);
|
||
maxX = std::max(maxX, right);
|
||
maxY = std::max(maxY, bottom);
|
||
found = true;
|
||
}
|
||
}
|
||
if (found) {
|
||
outX = minX;
|
||
outY = minY;
|
||
outW = maxX - minX;
|
||
outH = maxY - minY;
|
||
}
|
||
return found;
|
||
}
|
||
};
|