Files
Crosspoint/lib/Epub/Epub/blocks/BlockStyle.h
T
Jake Kenneally 9b04c2ec76 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
2026-02-09 08:31:52 +11:00

98 lines
4.7 KiB
C++

#pragma once
#include <cstdint>
#include "Epub/css/CssStyle.h"
/**
* BlockStyle - Block-level styling properties
*/
struct BlockStyle {
CssTextAlign alignment = CssTextAlign::Justify;
// Spacing (in pixels)
int16_t marginTop = 0;
int16_t marginBottom = 0;
int16_t marginLeft = 0;
int16_t marginRight = 0;
int16_t paddingTop = 0; // treated same as margin for rendering
int16_t paddingBottom = 0; // treated same as margin for rendering
int16_t paddingLeft = 0; // treated same as margin for rendering
int16_t paddingRight = 0; // treated same as margin for rendering
int16_t textIndent = 0;
bool textIndentDefined = false; // true if text-indent was explicitly set in CSS
bool textAlignDefined = false; // true if text-align was explicitly set in CSS
// Combined horizontal insets (margin + padding)
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
[[nodiscard]] int16_t totalHorizontalInset() const { return leftInset() + rightInset(); }
// Combine with another block style. Useful for parent -> child styles, where the child style should be
// applied on top of the parent's style to get the combined style.
BlockStyle getCombinedBlockStyle(const BlockStyle& child) const {
BlockStyle combinedBlockStyle;
combinedBlockStyle.marginTop = static_cast<int16_t>(child.marginTop + marginTop);
combinedBlockStyle.marginBottom = static_cast<int16_t>(child.marginBottom + marginBottom);
combinedBlockStyle.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
combinedBlockStyle.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
combinedBlockStyle.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
combinedBlockStyle.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
combinedBlockStyle.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
combinedBlockStyle.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
// Text indent: use child's if defined
if (child.textIndentDefined) {
combinedBlockStyle.textIndent = child.textIndent;
combinedBlockStyle.textIndentDefined = true;
} else {
combinedBlockStyle.textIndent = textIndent;
combinedBlockStyle.textIndentDefined = textIndentDefined;
}
// Text align: use child's if defined
if (child.textAlignDefined) {
combinedBlockStyle.alignment = child.alignment;
combinedBlockStyle.textAlignDefined = true;
} else {
combinedBlockStyle.alignment = alignment;
combinedBlockStyle.textAlignDefined = textAlignDefined;
}
return combinedBlockStyle;
}
// 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,
const uint16_t viewportWidth = 0) {
BlockStyle blockStyle;
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, vw);
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw);
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw);
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw);
// 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) {
blockStyle.alignment = blockStyle.textAlignDefined ? cssStyle.textAlign : CssTextAlign::Justify;
} else {
blockStyle.alignment = paragraphAlignment;
}
return blockStyle;
}
};