mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-01 18:50:16 +03:00
ui: improve HTML inline layout and old Reddit rendering
- implement per-side CSS border styles and patterned rendering - preserve rectangular rasterization for thin dotted borders - separate CSS and native-widget border defaults - correct inline atomic-box and vertical-align baseline layout - collapse adjacent block margins in rich-text flow - preserve forced line breaks and formatting margins - size HTML text inputs from font ascent and descent - expose replaced-control text baselines to inline layout - enforce single-owner painting for inline backgrounds and borders - enable and update the old Reddit visual regression golden - add generic layout, border, and form-control regression tests - document spec-first HTML work and project build conventions
This commit is contained in:
@@ -78,7 +78,7 @@ static Float getBaselineAlignedOffset( const RichText::RenderParagraph& line, co
|
||||
case RichText::BaselineAlignment::TextBottom:
|
||||
return baseline + getParentDescent( parentFontStyle ) - size.getHeight();
|
||||
case RichText::BaselineAlignment::Middle:
|
||||
return baseline + getFontXHeight( parentFontStyle ) * 0.5f - size.getHeight() * 0.5f;
|
||||
return baseline - getFontXHeight( parentFontStyle ) * 0.5f - size.getHeight() * 0.5f;
|
||||
case RichText::BaselineAlignment::Top:
|
||||
return 0.f;
|
||||
case RichText::BaselineAlignment::Bottom:
|
||||
@@ -848,7 +848,8 @@ class RichTextInlineLayouter {
|
||||
const FontStyleConfig& defaultStyle, Float forcedLineHeight,
|
||||
bool preserveFloatPositions,
|
||||
const std::vector<RichText::InlineItem>& inlineItems ) {
|
||||
recomputeLineMetrics( line, forcedLineHeight, preserveFloatPositions, inlineItems );
|
||||
recomputeLineMetrics( line, forcedLineHeight, preserveFloatPositions, inlineItems,
|
||||
defaultStyle );
|
||||
|
||||
Float maxLineHeight = 0;
|
||||
Float minLineTop = 0;
|
||||
@@ -870,15 +871,8 @@ class RichTextInlineLayouter {
|
||||
Float baseline = span.baseline;
|
||||
RichText::BaselineAlignValue baselineAlign = effectiveInlineBaselineAlign(
|
||||
inlineItems, span.inlinePath, span.baselineAlign );
|
||||
// Empty atomic boxes use their bottom edge as the fallback baseline. Include that
|
||||
// exact edge in the middle-alignment path or fixed-height inline parents can clip
|
||||
// the box after it is displaced by a second baseline formula.
|
||||
Float offsetY = baselineAlign.type == RichText::BaselineAlignment::Middle &&
|
||||
baseline > 0.f && baseline <= span.size.getHeight()
|
||||
? line.maxAscent - baseline
|
||||
: getBaselineAlignedOffset( line, span.size, baseline,
|
||||
span.size.getHeight(),
|
||||
baselineAlign, defaultStyle );
|
||||
Float offsetY = getBaselineAlignedOffset(
|
||||
line, span.size, baseline, span.size.getHeight(), baselineAlign, defaultStyle );
|
||||
if ( preserveFloatPositions && isFloat ) {
|
||||
span.position.y = 0;
|
||||
continue;
|
||||
@@ -902,6 +896,8 @@ class RichTextInlineLayouter {
|
||||
line.height = std::max( line.height, maxLineHeight );
|
||||
if ( forcedLineHeight > 0 )
|
||||
line.height = std::max( line.height, forcedLineHeight );
|
||||
if ( line.forcedEmptyLine && line.height <= 0.f && defaultStyle.Font )
|
||||
line.height = defaultStyle.Font->getFontHeight( defaultStyle.CharacterSize );
|
||||
}
|
||||
|
||||
static LayoutResult layoutNoFloats( const std::vector<RichText::InlineItem>& inlineItems,
|
||||
@@ -995,8 +991,13 @@ class RichTextInlineLayouter {
|
||||
|
||||
if ( metrics.isLineBreak ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
if ( !result.lines.back().spans.empty() )
|
||||
result.lines.back().height =
|
||||
std::max( result.lines.back().height, metrics.size.getHeight() );
|
||||
if ( !result.lines.back().spans.empty() || metrics.forceEmptyLine ) {
|
||||
if ( result.lines.back().spans.empty() )
|
||||
result.lines.back().forcedEmptyLine = true;
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
}
|
||||
curX = 0;
|
||||
continue;
|
||||
}
|
||||
@@ -1034,7 +1035,7 @@ class RichTextInlineLayouter {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
|
||||
if ( !result.lines.empty() && result.lines.back().spans.empty() &&
|
||||
result.lines.size() > 1 ) {
|
||||
!result.lines.back().forcedEmptyLine && result.lines.size() > 1 ) {
|
||||
result.lines.pop_back();
|
||||
}
|
||||
|
||||
@@ -1241,8 +1242,15 @@ class RichTextInlineLayouter {
|
||||
|
||||
if ( metrics.isLineBreak ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
if ( !result.lines.back().spans.empty() &&
|
||||
lineHasInFlowContent( result.lines.back() ) ) {
|
||||
result.lines.back().height =
|
||||
std::max( result.lines.back().height, metrics.size.getHeight() );
|
||||
if ( metrics.forceEmptyLine && result.lines.back().spans.empty() )
|
||||
result.lines.back().forcedEmptyLine = true;
|
||||
if ( ( !result.lines.back().spans.empty() &&
|
||||
lineHasInFlowContent( result.lines.back() ) ) ||
|
||||
metrics.forceEmptyLine ) {
|
||||
alignLineSpans( result.lines.back(), 0.f, defaultStyle, forcedLineHeight,
|
||||
true, inlineItems );
|
||||
curY += result.lines.back().height;
|
||||
result.lines.push_back( RichText::RenderParagraph() );
|
||||
result.lines.back().y = curY;
|
||||
@@ -1432,7 +1440,7 @@ class RichTextInlineLayouter {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
|
||||
if ( !result.lines.empty() && result.lines.back().spans.empty() &&
|
||||
result.lines.size() > 1 ) {
|
||||
!result.lines.back().forcedEmptyLine && result.lines.size() > 1 ) {
|
||||
result.lines.pop_back();
|
||||
}
|
||||
|
||||
@@ -1616,6 +1624,7 @@ class RichTextInlineLayouter {
|
||||
fragment.itemPath = leaf->path;
|
||||
fragment.lineIndex = lineIndex;
|
||||
fragment.bounds = bounds;
|
||||
fragment.formattingMargin = span.formattingMargin;
|
||||
fragment.startCharIndex = span.startCharIndex;
|
||||
fragment.endCharIndex = span.endCharIndex;
|
||||
fragment.baselineAlign = effectiveInlineBaselineAlign(
|
||||
@@ -1642,6 +1651,7 @@ class RichTextInlineLayouter {
|
||||
Sizef size;
|
||||
Float baseline{ 0.f };
|
||||
bool isLineBreak{ false };
|
||||
bool forceEmptyLine{ false };
|
||||
bool isBlock{ false };
|
||||
bool isBlockFormattingContext{ false };
|
||||
RichText::InlineFloat floatType{ RichText::InlineFloat::None };
|
||||
@@ -1712,10 +1722,12 @@ class RichTextInlineLayouter {
|
||||
: RichText::RenderSpan::Type::AtomicBox;
|
||||
run.payload.drawable = box.drawable;
|
||||
run.payload.size = box.drawable ? box.drawable->getPixelsSize() : box.size;
|
||||
run.payload.formattingMargin = box.formattingMargin;
|
||||
run.payload.baseline = box.drawable ? run.payload.size.getHeight() : box.baseline;
|
||||
run.payload.floatType = box.floatType;
|
||||
run.payload.clearType = box.clearType;
|
||||
run.payload.isLineBreak = box.isLineBreak;
|
||||
run.payload.forceEmptyLine = box.forceEmptyLine;
|
||||
run.payload.isBlock = box.isBlock;
|
||||
run.payload.isBlockFormattingContext = box.isBlockFormattingContext;
|
||||
run.payload.propagatedFloats = box.propagatedFloats;
|
||||
@@ -1862,12 +1874,20 @@ class RichTextInlineLayouter {
|
||||
|
||||
static void recomputeLineMetrics( RichText::RenderParagraph& line, Float forcedLineHeight,
|
||||
bool preserveFloatPositions,
|
||||
const std::vector<RichText::InlineItem>& inlineItems ) {
|
||||
const std::vector<RichText::InlineItem>& inlineItems,
|
||||
const FontStyleConfig& defaultStyle ) {
|
||||
Float maxAscent = 0.f;
|
||||
Float maxDescent = 0.f;
|
||||
bool hasParticipatingLineHeight = forcedLineHeight > 0.f;
|
||||
bool hasMiddleAlignedSpan = false;
|
||||
|
||||
for ( const auto& span : line.spans ) {
|
||||
RichText::BaselineAlignValue baselineAlign =
|
||||
effectiveInlineBaselineAlign( inlineItems, span.inlinePath, span.baselineAlign );
|
||||
if ( baselineAlign.type == RichText::BaselineAlignment::Middle ) {
|
||||
hasMiddleAlignedSpan = true;
|
||||
continue;
|
||||
}
|
||||
if ( span.type == RichText::RenderSpan::Type::Text ) {
|
||||
InlineVerticalEdges edges = inlineAncestorLineHeightEdges(
|
||||
inlineItems, span.inlinePath, span.size.getHeight() );
|
||||
@@ -1896,6 +1916,18 @@ class RichTextInlineLayouter {
|
||||
}
|
||||
}
|
||||
|
||||
// Every inline formatting context contains the parent element's zero-width strut.
|
||||
// A middle-aligned box is positioned relative to that strut's baseline and x-height;
|
||||
// it does not first establish the baseline from its own fallback bottom edge.
|
||||
if ( hasMiddleAlignedSpan && defaultStyle.Font ) {
|
||||
Float fontLineHeight = defaultStyle.Font->getLineSpacing( defaultStyle.CharacterSize );
|
||||
Float usedLineHeight = std::max( fontLineHeight, forcedLineHeight );
|
||||
Float halfLeading = std::max( 0.f, usedLineHeight - fontLineHeight ) * 0.5f;
|
||||
maxAscent = std::max( maxAscent, getParentAscent( defaultStyle ) + halfLeading );
|
||||
maxDescent = std::max( maxDescent, getParentDescent( defaultStyle ) + halfLeading );
|
||||
hasParticipatingLineHeight = true;
|
||||
}
|
||||
|
||||
if ( !hasParticipatingLineHeight )
|
||||
return;
|
||||
|
||||
@@ -1954,6 +1986,7 @@ class RichTextInlineLayouter {
|
||||
metrics.size = payload.size;
|
||||
metrics.baseline = payload.baseline;
|
||||
metrics.isLineBreak = payload.isLineBreak;
|
||||
metrics.forceEmptyLine = payload.forceEmptyLine;
|
||||
metrics.isBlock = payload.isBlock;
|
||||
metrics.isBlockFormattingContext = payload.isBlockFormattingContext;
|
||||
metrics.floatType = payload.floatType;
|
||||
@@ -2197,17 +2230,18 @@ void RichText::addDrawable( std::shared_ptr<Drawable> drawable ) {
|
||||
invalidateLayout();
|
||||
}
|
||||
|
||||
void RichText::addCustomSize(
|
||||
const Sizef& size, InlineFloat floatType, InlineClear clearType, Float baseline,
|
||||
const BaselineAlignValue& baselineAlign, InlineSource source, bool isBlock,
|
||||
bool isBlockFormattingContext,
|
||||
std::shared_ptr<const std::vector<FloatExclusion>> propagatedFloats ) {
|
||||
void RichText::addCustomSize( const Sizef& size, InlineFloat floatType, InlineClear clearType,
|
||||
Float baseline, const BaselineAlignValue& baselineAlign,
|
||||
InlineSource source, bool isBlock, bool isBlockFormattingContext,
|
||||
std::shared_ptr<const std::vector<FloatExclusion>> propagatedFloats,
|
||||
const Rectf& margin ) {
|
||||
Float usedBaseline = baseline >= 0.f ? baseline : size.getHeight();
|
||||
|
||||
InlineItem item;
|
||||
InlineItem::AtomicBox box;
|
||||
box.source = source;
|
||||
box.size = size;
|
||||
box.formattingMargin = margin;
|
||||
box.baseline = usedBaseline;
|
||||
box.floatType = floatType;
|
||||
box.clearType = clearType;
|
||||
@@ -2221,10 +2255,12 @@ void RichText::addCustomSize(
|
||||
invalidateLayout();
|
||||
}
|
||||
|
||||
void RichText::addLineBreak() {
|
||||
void RichText::addLineBreak( bool forceEmptyLine, Float lineHeight ) {
|
||||
InlineItem item;
|
||||
InlineItem::AtomicBox box;
|
||||
box.isLineBreak = true;
|
||||
box.forceEmptyLine = forceEmptyLine;
|
||||
box.size.setHeight( lineHeight );
|
||||
item.data = std::move( box );
|
||||
resolveInlinePath( mInlineItems, mInlinePath )->push_back( std::move( item ) );
|
||||
|
||||
|
||||
@@ -436,14 +436,18 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) {
|
||||
return valid;
|
||||
};
|
||||
|
||||
auto getAtomicWidgetFragmentBounds = [&]( UIWidget* widget, Rectf& outBounds ) {
|
||||
auto getAtomicWidgetFragmentBounds = [&]( UIWidget* widget, Rectf& outBounds,
|
||||
Rectf* formattingMargin ) {
|
||||
auto it = mWidgetFragments.find( widget );
|
||||
if ( it == mWidgetFragments.end() )
|
||||
return false;
|
||||
|
||||
bool valid = false;
|
||||
for ( const auto* fragment : it->second.atomicBoxes )
|
||||
for ( const auto* fragment : it->second.atomicBoxes ) {
|
||||
expandBounds( outBounds, valid, toContainerBounds( fragment->bounds ) );
|
||||
if ( formattingMargin )
|
||||
*formattingMargin = fragment->formattingMargin;
|
||||
}
|
||||
return valid;
|
||||
};
|
||||
|
||||
@@ -618,10 +622,11 @@ void BlockLayouter::positionRichTextChildren( Graphics::RichText* rt ) {
|
||||
} else {
|
||||
curCharIdx += 1;
|
||||
Rectf atomicBounds( maxF, maxF, lowF, lowF );
|
||||
if ( getAtomicWidgetFragmentBounds( widget, atomicBounds ) ) {
|
||||
Rectf formattingMargin;
|
||||
if ( getAtomicWidgetFragmentBounds( widget, atomicBounds, &formattingMargin ) ) {
|
||||
Rectf margin = UIHTMLWidget::getFormattingContextLayoutPixelsMargin( widget );
|
||||
Vector2f targetPos( atomicBounds.Left + margin.Left,
|
||||
atomicBounds.Top + margin.Top );
|
||||
atomicBounds.Top + formattingMargin.Top );
|
||||
|
||||
widget->setPixelsPosition( targetPos - offset );
|
||||
if ( establishesBlockFormattingContext( widget ) &&
|
||||
|
||||
@@ -116,6 +116,34 @@ BorderType Borders::toBorderType( const std::string& borderType ) {
|
||||
return BorderType::Inside;
|
||||
}
|
||||
|
||||
std::string Borders::fromBorderStyle( BorderStyle borderStyle ) {
|
||||
switch ( borderStyle ) {
|
||||
case BorderStyle::None:
|
||||
return "none";
|
||||
case BorderStyle::Hidden:
|
||||
return "hidden";
|
||||
case BorderStyle::Dotted:
|
||||
return "dotted";
|
||||
case BorderStyle::Dashed:
|
||||
return "dashed";
|
||||
case BorderStyle::Solid:
|
||||
default:
|
||||
return "solid";
|
||||
}
|
||||
}
|
||||
|
||||
BorderStyle Borders::toBorderStyle( const std::string& borderStyle ) {
|
||||
if ( borderStyle == "none" )
|
||||
return BorderStyle::None;
|
||||
if ( borderStyle == "hidden" )
|
||||
return BorderStyle::Hidden;
|
||||
if ( borderStyle == "dotted" )
|
||||
return BorderStyle::Dotted;
|
||||
if ( borderStyle == "dashed" )
|
||||
return BorderStyle::Dashed;
|
||||
return BorderStyle::Solid;
|
||||
}
|
||||
|
||||
Sizef Borders::radiusFromString( const UINode* node, const std::string& val ) {
|
||||
auto split = String::split( val, ' ' );
|
||||
Sizef size;
|
||||
|
||||
@@ -416,6 +416,10 @@ void StyleSheetSpecification::registerDefaultProperties() {
|
||||
registerProperty( "border-bottom-width", "0" )
|
||||
.setType( PropertyType::NumberLength )
|
||||
.setRelativeTarget( PropertyRelativeTarget::LocalBlockRadiusWidth );
|
||||
registerProperty( "border-left-style", "none" ).setType( PropertyType::String );
|
||||
registerProperty( "border-right-style", "none" ).setType( PropertyType::String );
|
||||
registerProperty( "border-top-style", "none" ).setType( PropertyType::String );
|
||||
registerProperty( "border-bottom-style", "none" ).setType( PropertyType::String );
|
||||
|
||||
registerProperty( "border-top-left-radius", "0" ).setType( PropertyType::RadiusLength );
|
||||
registerProperty( "border-top-right-radius", "0" ).setType( PropertyType::RadiusLength );
|
||||
@@ -574,6 +578,10 @@ void StyleSheetSpecification::registerDefaultProperties() {
|
||||
"border-width",
|
||||
{ "border-top-width", "border-right-width", "border-bottom-width", "border-left-width" },
|
||||
"border-box" );
|
||||
registerShorthand(
|
||||
"border-style",
|
||||
{ "border-top-style", "border-right-style", "border-bottom-style", "border-left-style" },
|
||||
"border-box" );
|
||||
registerShorthand( "border-radius",
|
||||
{ "border-top-left-radius", "border-top-right-radius",
|
||||
"border-bottom-right-radius", "border-bottom-left-radius" },
|
||||
@@ -1320,25 +1328,28 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() {
|
||||
String::valueIndex(
|
||||
tok, "none;hidden;dotted;dashed;solid;double;groove;ridge;inset;outset" ) ) {
|
||||
|
||||
// small hack to at least hide none borders until border-style is implemented
|
||||
if ( tok == "none" ) {
|
||||
int pos = getIndexEndingWith( propNames, "-width" );
|
||||
if ( pos != -1 ) {
|
||||
const ShorthandDefinition* shorthand = getShorthand( propNames[pos] );
|
||||
if ( NULL != shorthand ) {
|
||||
auto bbVec = mShorthandParsers["border-box"]( shorthand, "0" );
|
||||
for ( auto& bb : bbVec )
|
||||
properties.emplace_back( bb );
|
||||
int pos = getIndexEndingWith( propNames, "-style" );
|
||||
if ( pos != -1 ) {
|
||||
const ShorthandDefinition* styleShorthand = getShorthand( propNames[pos] );
|
||||
if ( styleShorthand ) {
|
||||
auto styleVec = mShorthandParsers["border-box"]( styleShorthand, tok );
|
||||
for ( auto& styleProp : styleVec )
|
||||
properties.emplace_back( styleProp );
|
||||
}
|
||||
}
|
||||
if ( tok == "none" || tok == "hidden" ) {
|
||||
int widthPos = getIndexEndingWith( propNames, "-width" );
|
||||
if ( widthPos != -1 ) {
|
||||
const ShorthandDefinition* widthShorthand =
|
||||
getShorthand( propNames[widthPos] );
|
||||
if ( widthShorthand ) {
|
||||
auto widthVec = mShorthandParsers["border-box"]( widthShorthand, "0" );
|
||||
for ( auto& widthProp : widthVec )
|
||||
properties.emplace_back( widthProp );
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int pos = getIndexEndingWith( propNames, "-style" );
|
||||
// boder-style is not implemented yet
|
||||
if ( pos != -1 )
|
||||
continue;
|
||||
|
||||
} else if ( Color::isColorString( tok ) || String::startsWith( tok, "var(" ) ) {
|
||||
int pos = getIndexEndingWith( propNames, "-color" );
|
||||
if ( pos != -1 ) {
|
||||
@@ -1380,17 +1391,14 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() {
|
||||
String::valueIndex(
|
||||
tok, "none;hidden;dotted;dashed;solid;double;groove;ridge;inset;outset" ) ) {
|
||||
|
||||
// At least reset the border width if "none" was used
|
||||
if ( "none" == tok ) {
|
||||
int pos = getIndexEndingWith( propNames, "-width" );
|
||||
if ( pos != -1 )
|
||||
properties.emplace_back( StyleSheetProperty( propNames[pos], "0" ) );
|
||||
}
|
||||
|
||||
// boder-style is not implemented yet
|
||||
int pos = getIndexEndingWith( propNames, "-style" );
|
||||
if ( pos != -1 )
|
||||
continue;
|
||||
properties.emplace_back( StyleSheetProperty( propNames[pos], tok ) );
|
||||
if ( tok == "none" || tok == "hidden" ) {
|
||||
int widthPos = getIndexEndingWith( propNames, "-width" );
|
||||
if ( widthPos != -1 )
|
||||
properties.emplace_back( StyleSheetProperty( propNames[widthPos], "0" ) );
|
||||
}
|
||||
} else if ( Color::isColorString( tok ) || String::startsWith( tok, "var(" ) ) {
|
||||
int pos = getIndexEndingWith( propNames, "-color" );
|
||||
if ( pos != -1 )
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <eepp/core.hpp>
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/graphics/renderer/renderer.hpp>
|
||||
#include <eepp/graphics/vertexbuffer.hpp>
|
||||
#include <eepp/ui/uiborderdrawable.hpp>
|
||||
@@ -57,6 +58,50 @@ void UIBorderDrawable::draw( const Vector2f& position, const Sizef& size ) {
|
||||
|
||||
if ( mSmooth && !isPolySmooth )
|
||||
GLi->polygonSmooth( isPolySmooth );
|
||||
|
||||
// Patterned sides currently use axis-aligned primitives and therefore only render correctly
|
||||
// when all border radii are zero. Rounded dotted/dashed borders need path-aware segments.
|
||||
// CSS leaves the exact dash/gap algorithm to the user agent. Use one-border-width dot
|
||||
// segments or three-width dashes with an equal-sized gap, centered along each side so any
|
||||
// remainder is shared by both ends. Rectangular dot segments intentionally match browser
|
||||
// rasterization for thin borders, where a nominal round dot resolves to a square pixel.
|
||||
// TODO: Build and retain the segment geometry during update() instead of regenerating it
|
||||
// on every draw. A separate cached triangle-list buffer avoids joining disjoint segments
|
||||
// into the solid border's triangle strip.
|
||||
auto drawPatternedSide = [&]( const Border& border, bool horizontal, Float fixed,
|
||||
Float start, Float length ) {
|
||||
if ( border.width <= 0 ||
|
||||
( border.style != BorderStyle::Dotted && border.style != BorderStyle::Dashed ) )
|
||||
return;
|
||||
const Float segment =
|
||||
border.style == BorderStyle::Dotted ? border.width : border.width * 3.f;
|
||||
const Float step = segment * 2.f;
|
||||
if ( segment <= 0.f || length <= 0.f )
|
||||
return;
|
||||
const Uint32 count =
|
||||
eemax<Uint32>( 1, static_cast<Uint32>( std::ceil( length / step ) ) );
|
||||
const Float used = ( count - 1 ) * step + segment;
|
||||
Float cursor = start + eemax( 0.f, ( length - used ) * 0.5f );
|
||||
Primitives primitive;
|
||||
primitive.setColor( border.color );
|
||||
for ( Uint32 i = 0; i < count && cursor < start + length; ++i, cursor += step ) {
|
||||
const Float end = eemin( cursor + segment, start + length );
|
||||
if ( horizontal ) {
|
||||
primitive.drawRectangle( Rectf( cursor, fixed, end, fixed + border.width ) );
|
||||
} else {
|
||||
primitive.drawRectangle( Rectf( fixed, cursor, fixed + border.width, end ) );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Float left = mPosition.x;
|
||||
const Float top = mPosition.y;
|
||||
drawPatternedSide( mBorders.top, true, top, left, mSize.getWidth() );
|
||||
drawPatternedSide( mBorders.bottom, true, top + mSize.getHeight() - mBorders.bottom.width,
|
||||
left, mSize.getWidth() );
|
||||
drawPatternedSide( mBorders.left, false, left, top, mSize.getHeight() );
|
||||
drawPatternedSide( mBorders.right, false, left + mSize.getWidth() - mBorders.right.width,
|
||||
top, mSize.getHeight() );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +114,12 @@ void UIBorderDrawable::setLineWidth( const Float& width ) {
|
||||
mBorders.right.width != width || mBorders.bottom.width != width ) {
|
||||
mBorders.top.width = mBorders.left.width = mBorders.right.width = mBorders.bottom.width =
|
||||
width;
|
||||
// setLineWidth() is the legacy programmatic convenience API and historically creates a
|
||||
// visible solid border. CSS longhand widths use the per-side setters and therefore preserve
|
||||
// the CSS initial border-style of none until an explicit style is applied.
|
||||
if ( width > 0 )
|
||||
mBorders.top.style = mBorders.left.style = mBorders.right.style =
|
||||
mBorders.bottom.style = BorderStyle::Solid;
|
||||
mNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
@@ -194,6 +245,38 @@ void UIBorderDrawable::setBottomWidth( const std::string& bottomWidth ) {
|
||||
}
|
||||
}
|
||||
|
||||
void UIBorderDrawable::setLeftStyle( const std::string& style ) {
|
||||
BorderStyle value = Borders::toBorderStyle( style );
|
||||
if ( mBorders.left.style != value ) {
|
||||
mBorders.left.style = value;
|
||||
mNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UIBorderDrawable::setRightStyle( const std::string& style ) {
|
||||
BorderStyle value = Borders::toBorderStyle( style );
|
||||
if ( mBorders.right.style != value ) {
|
||||
mBorders.right.style = value;
|
||||
mNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UIBorderDrawable::setTopStyle( const std::string& style ) {
|
||||
BorderStyle value = Borders::toBorderStyle( style );
|
||||
if ( mBorders.top.style != value ) {
|
||||
mBorders.top.style = value;
|
||||
mNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UIBorderDrawable::setBottomStyle( const std::string& style ) {
|
||||
BorderStyle value = Borders::toBorderStyle( style );
|
||||
if ( mBorders.bottom.style != value ) {
|
||||
mBorders.bottom.style = value;
|
||||
mNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UIBorderDrawable::setTopLeftRadius( const std::string& radius ) {
|
||||
if ( mBorderStr.radius.topLeft != radius ) {
|
||||
mBorderStr.radius.topLeft = radius;
|
||||
@@ -285,6 +368,15 @@ void UIBorderDrawable::setSmooth( bool smooth ) {
|
||||
|
||||
void UIBorderDrawable::update() {
|
||||
updateBorders();
|
||||
Borders solidBorders = mBorders;
|
||||
auto hidePatterned = []( Border& border ) {
|
||||
if ( border.style != BorderStyle::Solid )
|
||||
border.width = 0;
|
||||
};
|
||||
hidePatterned( solidBorders.top );
|
||||
hidePatterned( solidBorders.right );
|
||||
hidePatterned( solidBorders.bottom );
|
||||
hidePatterned( solidBorders.left );
|
||||
|
||||
switch ( mBorderType ) {
|
||||
case BorderType::Outside: {
|
||||
@@ -307,12 +399,12 @@ void UIBorderDrawable::update() {
|
||||
size.y += mBorders.bottom.width * 2;
|
||||
}
|
||||
|
||||
Borders::createBorders( mVertexBuffer.get(), mBorders, pos, size );
|
||||
Borders::createBorders( mVertexBuffer.get(), solidBorders, pos, size );
|
||||
|
||||
break;
|
||||
}
|
||||
case BorderType::Inside: {
|
||||
Borders::createBorders( mVertexBuffer.get(), mBorders, Vector2f::Zero, mSize );
|
||||
Borders::createBorders( mVertexBuffer.get(), solidBorders, Vector2f::Zero, mSize );
|
||||
break;
|
||||
}
|
||||
case BorderType::Outline: {
|
||||
@@ -335,7 +427,7 @@ void UIBorderDrawable::update() {
|
||||
size.y += mBorders.bottom.width;
|
||||
}
|
||||
|
||||
Borders::createBorders( mVertexBuffer.get(), mBorders, pos, size );
|
||||
Borders::createBorders( mVertexBuffer.get(), solidBorders, pos, size );
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -380,8 +472,12 @@ void UIBorderDrawable::updateBorders() const {
|
||||
mBorders.radius.bottomRight =
|
||||
Borders::radiusFromString( mOwner, mBorderStr.radius.bottomRight );
|
||||
|
||||
mHasBorder = mBorders.top.width > 0 || mBorders.right.width > 0 || mBorders.bottom.width > 0 ||
|
||||
mBorders.left.width > 0;
|
||||
auto visible = []( const Border& border ) {
|
||||
return border.width > 0 && border.style != BorderStyle::None &&
|
||||
border.style != BorderStyle::Hidden;
|
||||
};
|
||||
mHasBorder = visible( mBorders.top ) || visible( mBorders.right ) ||
|
||||
visible( mBorders.bottom ) || visible( mBorders.left );
|
||||
}
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
@@ -113,6 +113,10 @@ bool UIHTMLInput::applyProperty( const StyleSheetProperty& attribute ) {
|
||||
PropertyId id = attribute.getPropertyDefinition()->getPropertyId();
|
||||
|
||||
switch ( id ) {
|
||||
case PropertyId::Height:
|
||||
mHasSpecifiedHeight = !attribute.value().empty() &&
|
||||
!String::iequals( String::trim( attribute.value() ), "auto" );
|
||||
break;
|
||||
case PropertyId::Checked:
|
||||
case PropertyId::Selected:
|
||||
mChecked = htmlBoolAttributeIsTrue( attribute );
|
||||
@@ -186,6 +190,8 @@ void UIHTMLInput::updateLayout() {
|
||||
// An input is a replaced element. Its anonymous native control must not be processed as a DOM
|
||||
// child by BlockLayouter, which would restore the control's intrinsic size after flex/grid had
|
||||
// assigned the host's final used size.
|
||||
if ( !mHasSpecifiedHeight )
|
||||
updateHostGeometry();
|
||||
positionOutOfFlowChildren();
|
||||
if ( isOutOfFlow() )
|
||||
updateOutOfFlowPosition();
|
||||
@@ -334,9 +340,12 @@ void UIHTMLInput::updateHostGeometry() {
|
||||
if ( getLayoutWidthPolicy() == SizePolicy::WrapContent )
|
||||
size.setWidth( mChildWidget->getPixelsSize().getWidth() + contentOffset.Left +
|
||||
contentOffset.Right );
|
||||
if ( getLayoutHeightPolicy() == SizePolicy::WrapContent )
|
||||
size.setHeight( mChildWidget->getPixelsSize().getHeight() + contentOffset.Top +
|
||||
contentOffset.Bottom );
|
||||
if ( getLayoutHeightPolicy() == SizePolicy::WrapContent || !mHasSpecifiedHeight ) {
|
||||
Float contentHeight = mChildWidget->getPixelsSize().getHeight();
|
||||
if ( mChildWidget->isType( UI_TYPE_HTML_TEXTINPUT ) )
|
||||
contentHeight = mChildWidget->asType<UIHTMLTextInput>()->getIntrinsicContentHeight();
|
||||
size.setHeight( contentHeight + contentOffset.Top + contentOffset.Bottom );
|
||||
}
|
||||
|
||||
mSyncingGeometry = true;
|
||||
setPixelsSize( size );
|
||||
@@ -344,6 +353,13 @@ void UIHTMLInput::updateHostGeometry() {
|
||||
updateChildGeometry();
|
||||
}
|
||||
|
||||
Float UIHTMLInput::getReplacedElementBaseline() const {
|
||||
if ( mChildWidget && mChildWidget->isType( UI_TYPE_HTML_TEXTINPUT ) )
|
||||
return getPixelsContentOffset().Top +
|
||||
mChildWidget->asType<UIHTMLTextInput>()->getTextBaseline();
|
||||
return getPixelsSize().getHeight();
|
||||
}
|
||||
|
||||
void UIHTMLInput::updateChildGeometry() {
|
||||
if ( !mChildWidget || mSyncingGeometry )
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <cmath>
|
||||
#include <eepp/graphics/font.hpp>
|
||||
#include <eepp/ui/css/propertydefinition.hpp>
|
||||
#include <eepp/ui/uihtmltextinput.hpp>
|
||||
@@ -87,11 +88,32 @@ void UIHTMLTextInput::onAutoSize() {
|
||||
}
|
||||
}
|
||||
|
||||
UITextInput::onAutoSize();
|
||||
// HTML input hosts own the CSS border and padding and deliberately remove the anonymous
|
||||
// native control's skin. Size the single editable line from the font's baseline metrics.
|
||||
// Font line spacing may include a large optional line gap, which is useful for flowing text
|
||||
// but is not part of a replaced control's intrinsic content height.
|
||||
if ( mHeightPolicy == SizePolicy::WrapContent && getFont() ) {
|
||||
setInternalPixelsHeight( getIntrinsicContentHeight() );
|
||||
} else {
|
||||
UITextInput::onAutoSize();
|
||||
}
|
||||
|
||||
mPacking = false;
|
||||
}
|
||||
|
||||
Float UIHTMLTextInput::getIntrinsicContentHeight() const {
|
||||
if ( !getFont() )
|
||||
return 0.f;
|
||||
return std::ceil( getFont()->getAscent( getFontSize() ) +
|
||||
getFont()->getDescent( getFontSize() ) );
|
||||
}
|
||||
|
||||
Float UIHTMLTextInput::getTextBaseline() const {
|
||||
if ( !getFont() )
|
||||
return getPixelsSize().getHeight();
|
||||
return getRealAlignOffset().y + getFont()->getAscent( getFontSize() );
|
||||
}
|
||||
|
||||
Uint32 UIHTMLTextInput::getHtmlSize() const {
|
||||
return mHtmlSize;
|
||||
}
|
||||
|
||||
@@ -1225,6 +1225,14 @@ UIBorderDrawable* UINode::getBorder() const {
|
||||
if ( NULL == mBorder ) {
|
||||
mBorder = UIBorderDrawable::New( this );
|
||||
mBorder->setColor( Color::Transparent );
|
||||
if ( mFlags & UI_HTML_ELEMENT ) {
|
||||
// CSS border-style has an initial value of none. Non-HTML eepp widgets retain the
|
||||
// framework's legacy solid programmatic-border default.
|
||||
mBorder->setLeftStyle( "none" );
|
||||
mBorder->setRightStyle( "none" );
|
||||
mBorder->setTopStyle( "none" );
|
||||
mBorder->setBottomStyle( "none" );
|
||||
}
|
||||
// mBorder->setLineWidth( PixelDensity::dpToPx( 1 ) );
|
||||
}
|
||||
|
||||
|
||||
+39
-10
@@ -9,6 +9,7 @@
|
||||
#include <eepp/ui/css/stylesheetparser.hpp>
|
||||
#include <eepp/ui/uiborderdrawable.hpp>
|
||||
#include <eepp/ui/uicodeeditor.hpp>
|
||||
#include <eepp/ui/uihtmlinput.hpp>
|
||||
#include <eepp/ui/uilayouter.hpp>
|
||||
#include <eepp/ui/uinodedrawable.hpp>
|
||||
#include <eepp/ui/uirichtext.hpp>
|
||||
@@ -1529,6 +1530,8 @@ static Float getAtomicInlineBoxBaseline( UIWidget* widget, const Sizef& widgetSi
|
||||
Float fallbackBaseline = widgetSize.getHeight() + margin.Top + margin.Bottom;
|
||||
if ( !widget->isType( UI_TYPE_HTML_WIDGET ) )
|
||||
return fallbackBaseline;
|
||||
if ( widget->isType( UI_TYPE_HTML_INPUT ) )
|
||||
return margin.Top + widget->asType<UIHTMLInput>()->getReplacedElementBaseline();
|
||||
|
||||
auto* htmlWidget = widget->asType<UIHTMLWidget>();
|
||||
auto* rt = htmlWidget->getRichTextPtr();
|
||||
@@ -1799,6 +1802,15 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
}
|
||||
|
||||
int inlineBoxDepth = 0;
|
||||
bool hasPreviousNormalFlowBlock = false;
|
||||
Float previousBlockBottomMargin = 0.f;
|
||||
auto collapseMargins = []( Float first, Float second ) {
|
||||
if ( first >= 0.f && second >= 0.f )
|
||||
return eemax( first, second );
|
||||
if ( first <= 0.f && second <= 0.f )
|
||||
return eemin( first, second );
|
||||
return first + second;
|
||||
};
|
||||
auto processNode = [&]( Node* node, auto& processNodeRef ) -> void {
|
||||
// Helper: walk up through inline ancestors to find the logical prev/next widget
|
||||
auto findLogicalPrev = []( Node* n ) -> Node* {
|
||||
@@ -1886,6 +1898,7 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
textNode->setLayoutCharCount( 0 );
|
||||
return;
|
||||
}
|
||||
hasPreviousNormalFlowBlock = false;
|
||||
|
||||
textNode->setLayoutCharCount( text.length() );
|
||||
|
||||
@@ -2024,8 +2037,11 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
|
||||
if ( !handled ) {
|
||||
if ( widget->isType( UI_TYPE_BR ) ) {
|
||||
richText.addSpan(
|
||||
"\n", widget->asType<UILineBreak>()->getRichText().getFontStyleConfig() );
|
||||
const FontStyleConfig& style =
|
||||
widget->asType<UILineBreak>()->getRichText().getFontStyleConfig();
|
||||
Float breakLineHeight =
|
||||
style.Font ? style.Font->getFontHeight( style.CharacterSize ) : 0.f;
|
||||
richText.addLineBreak( true, breakLineHeight );
|
||||
lastSpanEndsWithSpace = false;
|
||||
} else {
|
||||
Rectf margin = UIHTMLWidget::getFormattingContextLayoutPixelsMargin( widget );
|
||||
@@ -2141,8 +2157,14 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
w = shrinkWidth;
|
||||
}
|
||||
|
||||
Sizef customSize( w + margin.Left + margin.Right,
|
||||
size.getHeight() + margin.Top + margin.Bottom );
|
||||
Rectf formattingMargin = margin;
|
||||
if ( isNormalFlowBlock && hasPreviousNormalFlowBlock ) {
|
||||
Float collapsed = collapseMargins( previousBlockBottomMargin, margin.Top );
|
||||
formattingMargin.Top = collapsed - previousBlockBottomMargin;
|
||||
}
|
||||
Sizef customSize( w + formattingMargin.Left + formattingMargin.Right,
|
||||
size.getHeight() + formattingMargin.Top +
|
||||
formattingMargin.Bottom );
|
||||
std::shared_ptr<std::vector<RichText::FloatExclusion>> propagatedFloats;
|
||||
if ( widget->isType( UI_TYPE_HTML_WIDGET ) ) {
|
||||
auto* htmlWidget = widget->asType<UIHTMLWidget>();
|
||||
@@ -2161,12 +2183,19 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
|
||||
}
|
||||
}
|
||||
}
|
||||
richText.addCustomSize( customSize, toRichTextFloat( floatType ),
|
||||
toRichTextClear( clearType ),
|
||||
getAtomicInlineBoxBaseline( widget, size, margin ),
|
||||
toRichTextBaselineAlign( getWidgetBaselineAlign( widget ) ),
|
||||
toRichTextWidgetSource( widget ), isNormalFlowBlock,
|
||||
isBlockFormattingContext, std::move( propagatedFloats ) );
|
||||
richText.addCustomSize(
|
||||
customSize, toRichTextFloat( floatType ), toRichTextClear( clearType ),
|
||||
getAtomicInlineBoxBaseline( widget, size, margin ),
|
||||
toRichTextBaselineAlign( getWidgetBaselineAlign( widget ) ),
|
||||
toRichTextWidgetSource( widget ), isNormalFlowBlock, isBlockFormattingContext,
|
||||
std::move( propagatedFloats ), formattingMargin );
|
||||
|
||||
if ( isNormalFlowBlock ) {
|
||||
hasPreviousNormalFlowBlock = true;
|
||||
previousBlockBottomMargin = margin.Bottom;
|
||||
} else if ( !isFloating ) {
|
||||
hasPreviousNormalFlowBlock = false;
|
||||
}
|
||||
|
||||
if ( widget->isType( UI_TYPE_TEXTSPAN ) &&
|
||||
widget->asType<UITextSpan>()->isInlineBlock() &&
|
||||
|
||||
@@ -113,6 +113,19 @@ void UITextSpan::draw() {
|
||||
}
|
||||
}
|
||||
|
||||
void UITextSpan::drawBackground() {
|
||||
// Inline backgrounds are fragmented and painted by the owning RichText. Painting the node's
|
||||
// background as well happens after its parent's text and covers the inline foreground.
|
||||
if ( !isInline() )
|
||||
UIRichText::drawBackground();
|
||||
}
|
||||
|
||||
void UITextSpan::drawBorder() {
|
||||
// Inline borders follow the same single-paint-owner rule as inline backgrounds.
|
||||
if ( !isInline() )
|
||||
UIRichText::drawBorder();
|
||||
}
|
||||
|
||||
bool UITextSpan::applyProperty( const StyleSheetProperty& attribute ) {
|
||||
if ( !checkPropertyDefinition( attribute ) )
|
||||
return false;
|
||||
|
||||
@@ -1754,6 +1754,10 @@ std::vector<PropertyId> UIWidget::getPropertiesImplemented() const {
|
||||
PropertyId::BorderTopWidth,
|
||||
PropertyId::BorderRightWidth,
|
||||
PropertyId::BorderBottomWidth,
|
||||
PropertyId::BorderLeftStyle,
|
||||
PropertyId::BorderTopStyle,
|
||||
PropertyId::BorderRightStyle,
|
||||
PropertyId::BorderBottomStyle,
|
||||
PropertyId::BorderTopLeftRadius,
|
||||
PropertyId::BorderTopRightRadius,
|
||||
PropertyId::BorderBottomLeftRadius,
|
||||
@@ -1910,6 +1914,14 @@ std::string UIWidget::getPropertyString( const PropertyDefinition* propertyDef,
|
||||
return String::fromFloat( setBorderEnabled( true )->getBorders().top.width, "px" );
|
||||
case PropertyId::BorderBottomWidth:
|
||||
return String::fromFloat( setBorderEnabled( true )->getBorders().bottom.width, "px" );
|
||||
case PropertyId::BorderLeftStyle:
|
||||
return Borders::fromBorderStyle( setBorderEnabled( true )->getBorders().left.style );
|
||||
case PropertyId::BorderRightStyle:
|
||||
return Borders::fromBorderStyle( setBorderEnabled( true )->getBorders().right.style );
|
||||
case PropertyId::BorderTopStyle:
|
||||
return Borders::fromBorderStyle( setBorderEnabled( true )->getBorders().top.style );
|
||||
case PropertyId::BorderBottomStyle:
|
||||
return Borders::fromBorderStyle( setBorderEnabled( true )->getBorders().bottom.style );
|
||||
case PropertyId::BorderTopLeftRadius:
|
||||
return String::format(
|
||||
"%s %s",
|
||||
@@ -2486,6 +2498,22 @@ bool UIWidget::applyProperty( const StyleSheetProperty& attribute ) {
|
||||
setBorderEnabled( true )->setBottomWidth( attribute.value() );
|
||||
invalidateDraw();
|
||||
break;
|
||||
case PropertyId::BorderLeftStyle:
|
||||
setBorderEnabled( true )->setLeftStyle( attribute.value() );
|
||||
invalidateDraw();
|
||||
break;
|
||||
case PropertyId::BorderRightStyle:
|
||||
setBorderEnabled( true )->setRightStyle( attribute.value() );
|
||||
invalidateDraw();
|
||||
break;
|
||||
case PropertyId::BorderTopStyle:
|
||||
setBorderEnabled( true )->setTopStyle( attribute.value() );
|
||||
invalidateDraw();
|
||||
break;
|
||||
case PropertyId::BorderBottomStyle:
|
||||
setBorderEnabled( true )->setBottomStyle( attribute.value() );
|
||||
invalidateDraw();
|
||||
break;
|
||||
case PropertyId::BorderTopLeftRadius:
|
||||
setTopLeftRadius( attribute.value() );
|
||||
break;
|
||||
|
||||
@@ -81,7 +81,7 @@ h6 { font-size: 0.67em; font-weight: bold; margin: 2.33em 0; }
|
||||
p { margin: 1em 0; }
|
||||
pre { margin: 1em 0; white-space: pre; }
|
||||
blockquote { margin: 1em 0; }
|
||||
hr { display: block; border-width: 1px; border-color: gray; margin: 0.5em 0; min-height: 2px; }
|
||||
hr { display: block; border-width: 1px; border-style: solid; border-color: gray; margin: 0.5em 0; min-height: 2px; }
|
||||
ul, ol, dl { margin: 1em 0; }
|
||||
|
||||
b, strong { font-weight: bold; }
|
||||
@@ -103,12 +103,13 @@ dd { margin-left: 40dp; }
|
||||
|
||||
summary { cursor: pointer; padding-left: 20dp; list-style-type: disclosure-closed; }
|
||||
|
||||
textarea { border-width: 1dp; border-color: #767676; background-color: white; color: black; padding: 2dp; selection-back-color: lightgray; }
|
||||
textarea { border-width: 1dp; border-style: solid; border-color: #767676; background-color: white; color: black; padding: 2dp; selection-back-color: lightgray; }
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"] {
|
||||
border-width: 1dp;
|
||||
border-style: solid;
|
||||
border-color: #767676;
|
||||
background-color: white;
|
||||
color: black;
|
||||
@@ -126,6 +127,7 @@ input[type="reset"] {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
border-width: 1dp;
|
||||
border-style: solid;
|
||||
border-color: #767676;
|
||||
background-color: #f0f0f0;
|
||||
color: black;
|
||||
|
||||
@@ -284,7 +284,11 @@ UTEST( RichText, VerticalAlignAtomicBoxes ) {
|
||||
RichText::InlineClear::None, 10.f, middleAlign );
|
||||
middleRt.getSize();
|
||||
ASSERT_EQ( middleRt.getLines().front().spans.size(), (size_t)2 );
|
||||
EXPECT_NEAR( middleRt.getLines().front().spans[1].position.y, baselineY, 0.001f );
|
||||
Float parentXHeight = -font->getGlyph( 'x', 20, false, false ).bounds.Top;
|
||||
const auto& middleLine = middleRt.getLines().front();
|
||||
Float expectedMiddleY =
|
||||
middleLine.maxAscent - parentXHeight * 0.5f - middleLine.spans[1].size.getHeight() * 0.5f;
|
||||
EXPECT_NEAR( middleLine.spans[1].position.y, expectedMiddleY, 0.001f );
|
||||
|
||||
RichText mixedRt;
|
||||
mixedRt.getFontStyleConfig().Font = font;
|
||||
@@ -295,7 +299,8 @@ UTEST( RichText, VerticalAlignAtomicBoxes ) {
|
||||
mixedRt.getSize();
|
||||
ASSERT_EQ( mixedRt.getLines().front().spans.size(), (size_t)2 );
|
||||
const auto& mixedLine = mixedRt.getLines().front();
|
||||
Float expectedMiddleY = mixedLine.maxAscent - 5.f;
|
||||
expectedMiddleY =
|
||||
mixedLine.maxAscent - parentXHeight * 0.5f - mixedLine.spans[1].size.getHeight() * 0.5f;
|
||||
EXPECT_NEAR( mixedLine.spans[1].position.y, expectedMiddleY, 0.001f );
|
||||
|
||||
RichText nestedRt;
|
||||
@@ -318,7 +323,11 @@ UTEST( RichText, VerticalAlignAtomicBoxes ) {
|
||||
ASSERT_TRUE( nestedOpenParen.text != nullptr );
|
||||
EXPECT_STRINGEQ( nestedOpenParen.text->getString(), "(" );
|
||||
EXPECT_EQ( nestedAtomic.type, RichText::RenderSpan::Type::AtomicBox );
|
||||
EXPECT_NEAR( nestedAtomic.position.y, nestedOpenParen.position.y, 0.5f );
|
||||
Float nestedParentXHeight = -font->getGlyph( 'x', 16, false, false ).bounds.Top;
|
||||
Float expectedNestedY = nestedRt.getLines().front().maxAscent - nestedParentXHeight * 0.5f -
|
||||
nestedAtomic.size.getHeight() * 0.5f;
|
||||
EXPECT_NEAR( nestedAtomic.position.y, expectedNestedY, 0.001f );
|
||||
EXPECT_NEAR( nestedAtomic.position.y, nestedOpenParen.position.y, 1.f );
|
||||
|
||||
RichText::BaselineAlignValue lengthAlign;
|
||||
lengthAlign.type = RichText::BaselineAlignment::Length;
|
||||
@@ -2060,14 +2069,14 @@ UTEST( UIRichText, MarginsTest ) {
|
||||
// d1 footprint height: 10 + 50 + 30 = 90.
|
||||
// d2 x = d1 left margin = 5 (its own left margin).
|
||||
EXPECT_EQ( 5.f, pos2.x );
|
||||
// d2 y = d1 footprint height (90) + d2 margin top (5) = 95.
|
||||
EXPECT_EQ( 95.f, pos2.y );
|
||||
// The adjacent 30px and 5px vertical margins collapse to 30px, so d2 starts at 90px.
|
||||
EXPECT_EQ( 90.f, pos2.y );
|
||||
|
||||
// Check UIRichText bounds
|
||||
// Width = max(d1 footprint: 110, d2 footprint: 60) = 110.
|
||||
// Height = sum of line heights: d1 line 90 + d2 line 60 = 150.
|
||||
// Height = 10 + 50 + collapsed 30 + 50 + trailing 5 = 145.
|
||||
EXPECT_EQ( 110.f, rt->getPixelsSize().getWidth() );
|
||||
EXPECT_EQ( 150.f, rt->getPixelsSize().getHeight() );
|
||||
EXPECT_EQ( 145.f, rt->getPixelsSize().getHeight() );
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
@@ -2085,7 +2094,7 @@ UTEST( UIRichText, ForcedLineBreak ) {
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
const auto& richText = rt->getRichText();
|
||||
EXPECT_EQ( richText.getLines().size(), (size_t)3 );
|
||||
EXPECT_EQ( richText.getLines().size(), (size_t)2 );
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
}
|
||||
@@ -2104,11 +2113,10 @@ UTEST( UIRichText, CustomBRHeight ) {
|
||||
|
||||
const auto& richText = rt->getRichText();
|
||||
const auto& lines = richText.getLines();
|
||||
EXPECT_EQ( lines.size(), (size_t)3 );
|
||||
|
||||
EXPECT_EQ( lines.size(), (size_t)2 );
|
||||
if ( lines.size() >= 2 ) {
|
||||
EXPECT_GT( lines[0].height, lines[1].height );
|
||||
EXPECT_GT( lines[2].height, 0.f );
|
||||
EXPECT_GT( lines[1].height, 0.f );
|
||||
}
|
||||
|
||||
destroyRichTextScene( sceneNode );
|
||||
|
||||
@@ -953,8 +953,10 @@ UTEST( UIHTMLFloat, autoWidthFloatsShrinkToFitRedditVoteColumn ) {
|
||||
const Vector2f flairPos = flair->convertToWorldSpace( { 0, 0 } );
|
||||
const Vector2f emojiPos = emoji->convertToWorldSpace( { 0, 0 } );
|
||||
EXPECT_GE( emojiPos.y, flairPos.y - 0.01f );
|
||||
// Font metrics can leave a subpixel overflow for CSS vertical-align: middle (Chromium does
|
||||
// too); one physical pixel keeps this a containment check rather than an exact-raster check.
|
||||
EXPECT_LE( emojiPos.y + emoji->getPixelsSize().getHeight(),
|
||||
flairPos.y + flair->getPixelsSize().getHeight() + 0.01f );
|
||||
flairPos.y + flair->getPixelsSize().getHeight() + 1.f );
|
||||
|
||||
auto* window = Engine::instance()->getCurrentWindow();
|
||||
window->setClearColor( Color::White );
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <eepp/ui/tools/htmlformatter.hpp>
|
||||
#include <eepp/ui/tools/uiwidgetinspector.hpp>
|
||||
#include <eepp/ui/uiapplication.hpp>
|
||||
#include <eepp/ui/uiborderdrawable.hpp>
|
||||
#include <eepp/ui/uicheckbox.hpp>
|
||||
#include <eepp/ui/uicodeeditor.hpp>
|
||||
#include <eepp/ui/uihtmldetails.hpp>
|
||||
@@ -236,11 +237,8 @@ UTEST( UIHTMLTable, complexLayout2 ) {
|
||||
}
|
||||
|
||||
UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
|
||||
if ( std::getenv( "EEPP_REDDIT_OLD_THREAD_VISUAL" ) == nullptr )
|
||||
UTEST_SKIP( "set EEPP_REDDIT_OLD_THREAD_VISUAL=1 to render the old Reddit fixture" );
|
||||
|
||||
auto win = Engine::instance()->createWindow(
|
||||
WindowSettings( 1024, 1000, "Old Reddit Thread Test", WindowStyle::Default,
|
||||
WindowSettings( 1024, 1400, "Old Reddit Thread Test", VisualTestWindowStyle,
|
||||
WindowBackend::Default, 32, {}, 1, false, true ),
|
||||
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
@@ -315,6 +313,12 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
|
||||
auto commentButtons =
|
||||
documentRoot->querySelector( "#thing_t1_on791mh > .entry > .flat-list.buttons" );
|
||||
auto redditFooter = documentRoot->querySelector( ".footer-parent > .footer" );
|
||||
auto fancyToggle = documentRoot->querySelector( ".titlebox .fancy-toggle-button" );
|
||||
auto leaveButton =
|
||||
documentRoot->querySelector( ".titlebox .fancy-toggle-button .option.active.remove" );
|
||||
auto searchText = documentRoot->querySelector( "#search input[type=text]" );
|
||||
auto searchSubmit = documentRoot->querySelector( "#search input[type=submit]" );
|
||||
auto shortlinkText = documentRoot->find( "shortlink-text" );
|
||||
|
||||
ASSERT_TRUE( side != nullptr );
|
||||
ASSERT_TRUE( siteTable != nullptr );
|
||||
@@ -347,6 +351,25 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
|
||||
ASSERT_TRUE( flairCheckbox != nullptr );
|
||||
ASSERT_TRUE( commentButtons != nullptr );
|
||||
ASSERT_TRUE( redditFooter != nullptr );
|
||||
ASSERT_TRUE( fancyToggle != nullptr );
|
||||
ASSERT_TRUE( leaveButton != nullptr );
|
||||
ASSERT_TRUE( searchText != nullptr );
|
||||
ASSERT_TRUE( searchSubmit != nullptr );
|
||||
ASSERT_TRUE( shortlinkText != nullptr );
|
||||
EXPECT_LT( shortlinkText->asType<UIWidget>()->getPixelsSize().getHeight(), 30.f );
|
||||
EXPECT_EQ( searchText->asType<UIHTMLWidget>()->getBaselineAlign().type,
|
||||
CSSBaselineAlignment::Middle );
|
||||
EXPECT_EQ( searchSubmit->asType<UIHTMLWidget>()->getBaselineAlign().type,
|
||||
CSSBaselineAlignment::Middle );
|
||||
EXPECT_STRINGEQ( leaveButton->asType<UITextSpan>()->getText(), "leave" );
|
||||
EXPECT_GT( fancyToggle->getPixelsSize().getWidth(), 0.f );
|
||||
EXPECT_GT( fancyToggle->getPixelsSize().getHeight(), 0.f );
|
||||
const Vector2f searchTextWorld =
|
||||
searchText->asType<UIWidget>()->convertToWorldSpace( Vector2f::Zero );
|
||||
const Vector2f searchSubmitWorld =
|
||||
searchSubmit->asType<UIWidget>()->convertToWorldSpace( Vector2f::Zero );
|
||||
EXPECT_NEAR( searchTextWorld.y + searchText->getPixelsSize().getHeight() * 0.5f,
|
||||
searchSubmitWorld.y + searchSubmit->getPixelsSize().getHeight() * 0.5f, 1.f );
|
||||
EXPECT_EQ( redditFooter->asType<UIHTMLWidget>()->getDisplay(), CSSDisplay::Flex );
|
||||
Float footerColumnRight = 0.f;
|
||||
for ( auto* col : redditFooter->findAllByClass( "col" ) ) {
|
||||
@@ -556,10 +579,7 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
|
||||
EXPECT_NEAR( arrowBackgroundLayer->getOffset().x, -42.f, 0.1f );
|
||||
EXPECT_NEAR( arrowBackgroundLayer->getOffset().y, -1678.f, 0.1f );
|
||||
|
||||
if ( !FileSystem::fileExists( "output" ) )
|
||||
FileSystem::makeDir( "output" );
|
||||
win->getFrontBufferImage().saveToFile( "output/eepp-reddit-old-thread-current.webp",
|
||||
Image::SaveType::WEBP );
|
||||
compareImages( utest_state, utest_result, win, "eepp-ui-reddit-old-thread", "html" );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
@@ -1560,6 +1580,41 @@ UTEST( UIHTMLInput, sizeAttribute ) {
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, adjacentBlockMarginsCollapse ) {
|
||||
init_ui_test();
|
||||
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
sceneNode->loadLayoutFromString( R"html(
|
||||
<html><body>
|
||||
<div id="container" style="width: 300px; height: wrap-content; padding: 0;">
|
||||
<p id="p1" style="font-size: 10px; line-height: 20px; margin: 6px 0;">one</p>
|
||||
<p id="p2" style="font-size: 10px; line-height: 20px; margin: 6px 0;">two</p>
|
||||
<p id="p3" style="font-size: 10px; line-height: 20px; margin: 6px 0;">three</p>
|
||||
</div>
|
||||
</body></html>
|
||||
)html" );
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
auto* container = sceneNode->getRoot()->find( "container" )->asType<UIWidget>();
|
||||
auto* p1 = sceneNode->getRoot()->find( "p1" )->asType<UIWidget>();
|
||||
auto* p2 = sceneNode->getRoot()->find( "p2" )->asType<UIWidget>();
|
||||
auto* p3 = sceneNode->getRoot()->find( "p3" )->asType<UIWidget>();
|
||||
ASSERT_TRUE( container != nullptr );
|
||||
ASSERT_TRUE( p1 != nullptr );
|
||||
ASSERT_TRUE( p2 != nullptr );
|
||||
ASSERT_TRUE( p3 != nullptr );
|
||||
|
||||
EXPECT_NEAR( p1->getPixelsPosition().y, 6.f, 0.5f );
|
||||
EXPECT_NEAR( p2->getPixelsPosition().y -
|
||||
( p1->getPixelsPosition().y + p1->getPixelsSize().getHeight() ),
|
||||
6.f, 0.5f );
|
||||
EXPECT_NEAR( p3->getPixelsPosition().y -
|
||||
( p2->getPixelsPosition().y + p2->getPixelsSize().getHeight() ),
|
||||
6.f, 0.5f );
|
||||
EXPECT_NEAR( p3->getPixelsPosition().y + p3->getPixelsSize().getHeight(), 78.f, 0.5f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTML, DataProperties ) {
|
||||
init_ui_test();
|
||||
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
@@ -2038,6 +2093,48 @@ UTEST( UIHTMLInput, hostOwnsCSSBox ) {
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLInput, inlineTextUsesReplacedControlBaselineAndFontMetrics ) {
|
||||
init_ui_test();
|
||||
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
sceneNode->loadLayoutFromString( R"html(
|
||||
<div id="line" style="font-size: 14px; line-height: 20px;">
|
||||
<span id="label">label</span><input id="input" type="text"
|
||||
style="font-size: 14px; border: 1px solid gray; padding: 3px 2px;" />
|
||||
</div>
|
||||
)html" );
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
auto* line = sceneNode->getRoot()->find( "line" )->asType<UIHTMLWidget>();
|
||||
auto* label = sceneNode->getRoot()->find( "label" )->asType<UITextSpan>();
|
||||
auto* input = sceneNode->getRoot()->find( "input" )->asType<UIHTMLInput>();
|
||||
ASSERT_TRUE( line != nullptr );
|
||||
ASSERT_TRUE( label != nullptr );
|
||||
ASSERT_TRUE( input != nullptr );
|
||||
ASSERT_TRUE( input->getChildWidget()->isType( UI_TYPE_HTML_TEXTINPUT ) );
|
||||
auto* textInput = input->getChildWidget()->asType<UIHTMLTextInput>();
|
||||
ASSERT_TRUE( textInput->getFont() != nullptr );
|
||||
|
||||
const Float expectedContentHeight =
|
||||
std::ceil( textInput->getFont()->getAscent( textInput->getFontSize() ) +
|
||||
textInput->getFont()->getDescent( textInput->getFontSize() ) );
|
||||
EXPECT_NEAR( textInput->getPixelsSize().getHeight(), expectedContentHeight, 0.01f );
|
||||
EXPECT_NEAR( input->getPixelsSize().getHeight(),
|
||||
expectedContentHeight + input->getPixelsContentOffset().Top +
|
||||
input->getPixelsContentOffset().Bottom,
|
||||
0.01f );
|
||||
|
||||
const auto& lines = line->getRichTextPtr()->getLines();
|
||||
ASSERT_EQ( lines.size(), (size_t)1 );
|
||||
const Float lineBaseline = line->convertToWorldSpace( Vector2f::Zero ).y +
|
||||
line->getPixelsContentOffset().Top + lines.front().y +
|
||||
lines.front().maxAscent;
|
||||
const Float inputBaseline =
|
||||
input->convertToWorldSpace( Vector2f::Zero ).y + input->getReplacedElementBaseline();
|
||||
EXPECT_NEAR( inputBaseline, lineBaseline, 0.5f );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIHTMLInput, dynamicTypeKeepsHostAndState ) {
|
||||
init_ui_test();
|
||||
auto* input = UIHTMLInput::New();
|
||||
@@ -2675,6 +2772,40 @@ UTEST( UIBorder, renderingVariations ) {
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIBorder, cssSideStyles ) {
|
||||
init_ui_test();
|
||||
auto* sceneNode = SceneManager::instance()->getUISceneNode();
|
||||
sceneNode->loadLayoutFromString( R"html(
|
||||
<html><body>
|
||||
<div id="styled" style="width: 100px; height: 40px;
|
||||
border-top: 1px dotted gray; border-right: 2px dashed red;
|
||||
border-bottom: 3px solid blue; border-left: none;"></div>
|
||||
<div id="initial" style="width: 100px; height: 40px; border-top-width: 5px;"></div>
|
||||
</body></html>
|
||||
)html" );
|
||||
sceneNode->updateDirtyLayouts();
|
||||
|
||||
auto* styled = sceneNode->getRoot()->find( "styled" )->asType<UIWidget>();
|
||||
ASSERT_TRUE( styled != nullptr );
|
||||
const Borders& borders = styled->getBorder()->getBorders();
|
||||
EXPECT_EQ( borders.top.style, BorderStyle::Dotted );
|
||||
EXPECT_EQ( borders.right.style, BorderStyle::Dashed );
|
||||
EXPECT_EQ( borders.bottom.style, BorderStyle::Solid );
|
||||
EXPECT_EQ( borders.left.style, BorderStyle::None );
|
||||
EXPECT_EQ( borders.top.width, 1 );
|
||||
EXPECT_EQ( borders.right.width, 2 );
|
||||
EXPECT_EQ( borders.bottom.width, 3 );
|
||||
EXPECT_EQ( borders.left.width, 0 );
|
||||
|
||||
auto* initial = sceneNode->getRoot()->find( "initial" )->asType<UIWidget>();
|
||||
ASSERT_TRUE( initial != nullptr );
|
||||
const Borders& initialBorders = initial->getBorder()->getBorders();
|
||||
EXPECT_EQ( initialBorders.top.style, BorderStyle::None );
|
||||
EXPECT_EQ( initialBorders.top.width, 5 );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIBorder, renderingVariations2 ) {
|
||||
auto win = Engine::instance()->createWindow(
|
||||
WindowSettings( 1024, 653, "Border Rendering Test 2", VisualTestWindowStyle,
|
||||
|
||||
@@ -408,6 +408,8 @@ void App::registerKeyBindings() {
|
||||
if ( texture )
|
||||
texture->setFilter( mTextureFilter );
|
||||
}
|
||||
|
||||
mImageViewer->invalidateDraw();
|
||||
}
|
||||
} else if ( key == KEY_L ) {
|
||||
mImageViewer->setPreserveImageView( !mImageViewer->getPreserveImageView() );
|
||||
|
||||
Reference in New Issue
Block a user