feat: Add percentage support to CSS properties (#738)
## Summary - Closes #730 **What is the goal of this PR?** - Adds percentage-based value support to CSS properties that accept percentages (padding, margin, text-indent) **What changes are included?** - Adds `Percent` as another CSS unit - Passes the viewport width to `fromCssStyle` so that we can resolve percentage-based values - Adds a fallback of using an emspace for text-indent if we have an unresolvable value for whatever reason ## Additional Context - This was missed in my CSS support feature, and the fallback when we encounter a percentage value is to use px instead. This means 5% (which would be ~30px on the screen) turns into 5px. When percentages are used in `text-indent`, this fallback behavior makes the indent look like a single space character. Whoops! 😬 My test EPUB has been updated [here](https://github.com/jdk2pq/css-test-epub) with percentage based CSS values at the end of the book. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_, Claude Code
This commit is contained in:
committed by
Dave Allie
parent
def1094411
commit
9e04eec072
@@ -64,21 +64,27 @@ struct BlockStyle {
|
||||
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
|
||||
// emSize is the current font line height, used for em/rem unit conversion
|
||||
// paragraphAlignment is the user's paragraphAlignment setting preference
|
||||
static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment) {
|
||||
static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment,
|
||||
const uint16_t viewportWidth = 0) {
|
||||
BlockStyle blockStyle;
|
||||
// Resolve all CssLength values to pixels using the current font's em size
|
||||
blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize);
|
||||
blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize);
|
||||
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize);
|
||||
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize);
|
||||
const float vw = viewportWidth;
|
||||
// Resolve all CssLength values to pixels using the current font's em size and viewport width
|
||||
blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw);
|
||||
blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw);
|
||||
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw);
|
||||
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw);
|
||||
|
||||
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize);
|
||||
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize);
|
||||
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize);
|
||||
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize);
|
||||
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw);
|
||||
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw);
|
||||
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw);
|
||||
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw);
|
||||
|
||||
blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize);
|
||||
blockStyle.textIndentDefined = cssStyle.hasTextIndent();
|
||||
// For textIndent: if it's a percentage we can't resolve (no viewport width),
|
||||
// leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used
|
||||
if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) {
|
||||
blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw);
|
||||
blockStyle.textIndentDefined = true;
|
||||
}
|
||||
blockStyle.textAlignDefined = cssStyle.hasTextAlign();
|
||||
// User setting overrides CSS, unless "Book's Style" alignment setting is selected
|
||||
if (paragraphAlignment == CssTextAlign::None) {
|
||||
|
||||
@@ -283,6 +283,8 @@ CssLength CssParser::interpretLength(const std::string& val) {
|
||||
unit = CssUnit::Rem;
|
||||
} else if (unitPart == "pt") {
|
||||
unit = CssUnit::Points;
|
||||
} else if (unitPart == "%") {
|
||||
unit = CssUnit::Percent;
|
||||
}
|
||||
// px and unitless default to Pixels
|
||||
|
||||
@@ -518,7 +520,7 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par
|
||||
// Cache serialization
|
||||
|
||||
// Cache format version - increment when format changes
|
||||
constexpr uint8_t CSS_CACHE_VERSION = 1;
|
||||
constexpr uint8_t CSS_CACHE_VERSION = 2;
|
||||
|
||||
bool CssParser::saveToCache(FsFile& file) const {
|
||||
if (!file) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings
|
||||
enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 };
|
||||
enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 };
|
||||
enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3, Percent = 4 };
|
||||
|
||||
// Represents a CSS length value with its unit, allowing deferred resolution to pixels
|
||||
struct CssLength {
|
||||
@@ -17,21 +17,32 @@ struct CssLength {
|
||||
// Convenience constructor for pixel values (most common case)
|
||||
explicit CssLength(const float pixels) : value(pixels) {}
|
||||
|
||||
// Returns true if this length can be resolved to pixels with the given context.
|
||||
// Percentage units require a non-zero containerWidth to resolve.
|
||||
[[nodiscard]] bool isResolvable(const float containerWidth = 0) const {
|
||||
return unit != CssUnit::Percent || containerWidth > 0;
|
||||
}
|
||||
|
||||
// Resolve to pixels given the current em size (font line height)
|
||||
[[nodiscard]] float toPixels(const float emSize) const {
|
||||
// containerWidth is needed for percentage units (e.g. viewport width)
|
||||
[[nodiscard]] float toPixels(const float emSize, const float containerWidth = 0) const {
|
||||
switch (unit) {
|
||||
case CssUnit::Em:
|
||||
case CssUnit::Rem:
|
||||
return value * emSize;
|
||||
case CssUnit::Points:
|
||||
return value * 1.33f; // Approximate pt to px conversion
|
||||
case CssUnit::Percent:
|
||||
return value * containerWidth / 100.0f;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve to int16_t pixels (for BlockStyle fields)
|
||||
[[nodiscard]] int16_t toPixelsInt16(const float emSize) const { return static_cast<int16_t>(toPixels(emSize)); }
|
||||
[[nodiscard]] int16_t toPixelsInt16(const float emSize, const float containerWidth = 0) const {
|
||||
return static_cast<int16_t>(toPixels(emSize, containerWidth));
|
||||
}
|
||||
};
|
||||
|
||||
// Font style options matching CSS font-style property
|
||||
|
||||
@@ -213,12 +213,12 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
|
||||
const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
|
||||
const auto userAlignmentBlockStyle =
|
||||
BlockStyle::fromCssStyle(cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment));
|
||||
const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle(
|
||||
cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth);
|
||||
|
||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
self->currentCssStyle = cssStyle;
|
||||
auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center);
|
||||
auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center, self->viewportWidth);
|
||||
headerBlockStyle.textAlignDefined = true;
|
||||
if (self->embeddedStyle && cssStyle.hasTextAlign()) {
|
||||
headerBlockStyle.alignment = cssStyle.textAlign;
|
||||
|
||||
Reference in New Issue
Block a user