Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions Source/HtmlRenderer/Core/Dom/CssBoxProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ internal abstract class CssBoxProperties
private string _left = "auto";
private string _counterReset = CssConstants.None;
private string _counterIncrement = CssConstants.None;
private string _outlineStyle = CssConstants.None;
private string _outlineColor = string.Empty;
private string _outlineWidth = "medium";
private string _lineHeight = "normal";
private string _listStyleType = "disc";
private string _listStyleImage = string.Empty;
Expand Down Expand Up @@ -740,6 +743,62 @@ public string CounterIncrement
set { _counterIncrement = value; }
}

public string OutlineStyle
{
get { return _outlineStyle; }
set { _outlineStyle = value; }
}

public string OutlineColor
{
get { return _outlineColor; }
set { _outlineColor = value; }
}

public string OutlineWidth
{
get { return _outlineWidth; }
set { _outlineWidth = value; }
}

/// <summary>
/// The resolved <c>outline-width</c> in pixels ("thin"/"medium"/"thick" keywords resolved, like
/// <see cref="ActualBorderTopWidth"/>), or 0 when <c>outline-style</c> is <c>none</c>/<c>hidden</c>
/// (an outline with a style of none/hidden never paints, regardless of width - CSS 2.1 §18.1's
/// invert/no-op default matches border's own "no style, no width" rule).
/// </summary>
public double ActualOutlineWidth
{
get
{
if (string.IsNullOrEmpty(OutlineStyle) || OutlineStyle == CssConstants.None || OutlineStyle == CssConstants.Hidden)
{
return 0;
}
return CssValueParser.GetActualBorderWidth(OutlineWidth, this);
}
}

/// <summary>
/// The resolved paint color for <c>outline-color</c> - falls back to this box's own
/// <see cref="ActualColor"/> (i.e. <c>currentColor</c>) when <c>outline-color</c> is unset, which
/// is what every real browser does today for the property's nominal <c>invert</c> initial value
/// (true color inversion is not implemented here).
/// </summary>
public RColor ActualOutlineColor
{
get
{
// "transparent" is OutlineColorProperty's own cascaded initial value (Color.Transparent) -
// treated the same as unset, since an invisible-by-default outline would defeat the point
// of the property entirely.
return string.IsNullOrEmpty(OutlineColor) || OutlineColor == "transparent"
|| OutlineColor.Equals("invert", StringComparison.OrdinalIgnoreCase)
? ActualColor
: GetActualColor(OutlineColor);
}
}

/// <summary>
/// This box's resolved named-counter values (CSS 2.1 §12.4), as of just after its own
/// <c>counter-reset</c>/<c>counter-increment</c> have been applied - populated once, by
Expand Down Expand Up @@ -1939,6 +1998,9 @@ protected void InheritStyle(CssBox p, bool everything)
_borderRightStyle = p._borderRightStyle;
_borderBottomStyle = p._borderBottomStyle;
_borderLeftStyle = p._borderLeftStyle;
_outlineStyle = p._outlineStyle;
_outlineColor = p._outlineColor;
_outlineWidth = p._outlineWidth;
_bottom = p._bottom;
_borderTopLeftRadius = p._borderTopLeftRadius;
_borderTopRightRadius = p._borderTopRightRadius;
Expand Down
60 changes: 60 additions & 0 deletions Source/HtmlRenderer/Core/Handlers/OutlineDrawHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"

using TheArtOfDev.HtmlRenderer.Adapters;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
using TheArtOfDev.HtmlRenderer.Core.Dom;
using TheArtOfDev.HtmlRenderer.Core.Utils;

namespace TheArtOfDev.HtmlRenderer.Core.Handlers
{
/// <summary>
/// Paints <c>outline</c> (CSS 2.1 §18.1) as a plain rectangular ring around the box's own border-box
/// edge - unlike <see cref="BordersDrawHandler"/>, an outline has no per-side bevel/corner-joining to
/// account for (it isn't part of the box model and doesn't affect layout), so this is a single flat
/// four-rectangle fill rather than a per-side polygon.<br/>
/// Deliberately a subset of the full property: only <c>solid</c> (and <c>auto</c>, treated the same
/// as <c>solid</c> per spec) is painted - <c>dotted</c>/<c>dashed</c>/<c>double</c>/<c>groove</c>/
/// <c>ridge</c>/<c>inset</c>/<c>outset</c> are not (a box with one of those styles simply paints no
/// outline, same as <c>none</c>). <c>outline-offset</c> is not implemented either (the CSS-OM here has
/// no <c>outline-offset</c> property at all to read a value from), so the ring always sits flush
/// against the border-box edge.
/// </summary>
internal static class OutlineDrawHandler
{
/// <summary>
/// Paints <paramref name="box"/>'s outline around <paramref name="borderBoxRect"/> (the same
/// border-box rectangle <see cref="BordersDrawHandler.DrawBoxBorders"/> was just given for this
/// line) if it has a paintable one.
/// </summary>
public static void Draw(RGraphics g, CssBox box, RRect borderBoxRect)
{
var width = box.ActualOutlineWidth;
if (width <= 0 || borderBoxRect.Width <= 0 || borderBoxRect.Height <= 0) return;

var style = box.OutlineStyle;
var isSolid = style == CssConstants.Solid || style == CssConstants.Auto;
if (!isSolid) return;

var brush = g.GetSolidBrush(box.ActualOutlineColor);

// Top band (full width, including the corners) ...
g.DrawRectangle(brush, borderBoxRect.X - width, borderBoxRect.Y - width, borderBoxRect.Width + 2 * width, width);
// ... bottom band ...
g.DrawRectangle(brush, borderBoxRect.X - width, borderBoxRect.Bottom, borderBoxRect.Width + 2 * width, width);
// ... left band (between the top/bottom bands, not overlapping their corners) ...
g.DrawRectangle(brush, borderBoxRect.X - width, borderBoxRect.Y, width, borderBoxRect.Height);
// ... right band.
g.DrawRectangle(brush, borderBoxRect.Right, borderBoxRect.Y, width, borderBoxRect.Height);
}
}
}
2 changes: 2 additions & 0 deletions Source/HtmlRenderer/Core/Paint/Content/HrFragmentPainter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using TheArtOfDev.HtmlRenderer.Adapters.Entities;
using TheArtOfDev.HtmlRenderer.Core.Dom;
using TheArtOfDev.HtmlRenderer.Core.Fragments;
using TheArtOfDev.HtmlRenderer.Core.Handlers;

namespace TheArtOfDev.HtmlRenderer.Core.Paint.Content
{
Expand All @@ -25,6 +26,7 @@ public void Paint(FragmentPainter painter, RGraphics g, BoxFragment fragment)
var rect = fragment.PrimaryRect;
rect.Offset(offset);
box.DrawHrContent(g, rect);
OutlineDrawHandler.Draw(g, box, rect);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public void Paint(FragmentPainter painter, RGraphics g, BoxFragment fragment)

box.PaintBackground(g, rect, true, true);
BordersDrawHandler.DrawBoxBorders(g, box, rect, true, true);
OutlineDrawHandler.Draw(g, box, rect);

DrawContent(g, fragment, painter.LiveTreeOffset(box.IsFixed));

Expand Down
1 change: 1 addition & 0 deletions Source/HtmlRenderer/Core/Paint/FragmentPainter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ private void PaintFragmentContent(RGraphics g, BoxFragment fragment)
{
box.PaintBackground(g, actualRect, i == 0, i == lines.Count - 1);
BordersDrawHandler.DrawBoxBorders(g, box, actualRect, i == 0, i == lines.Count - 1);
OutlineDrawHandler.Draw(g, box, actualRect);
}
}

Expand Down
16 changes: 16 additions & 0 deletions Source/HtmlRenderer/Core/Utils/CssUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ internal static class CssUtils
"page-break-inside", "break-inside", "break-before", "break-after", "page-break-before", "page-break-after",
"widows", "orphans", "page",
"left", "top", "right", "bottom", "counter-reset", "counter-increment",
"outline-style", "outline-color", "outline-width",
"width", "max-width", "height", "min-height", "max-height",
"background-color", "background-image", "background-position", "background-repeat",
"content", "color", "display", "direction", "empty-cells", "float", "clear", "box-sizing", "position",
Expand Down Expand Up @@ -180,6 +181,12 @@ public static string GetPropertyValue(CssBox cssBox, string propName)
return cssBox.CounterReset;
case "counter-increment":
return cssBox.CounterIncrement;
case "outline-style":
return cssBox.OutlineStyle;
case "outline-color":
return cssBox.OutlineColor;
case "outline-width":
return cssBox.OutlineWidth;
case "width":
return cssBox.Width;
case "max-width":
Expand Down Expand Up @@ -397,6 +404,15 @@ public static void SetPropertyValue(CssBox cssBox, string propName, string value
case "counter-increment":
cssBox.CounterIncrement = value;
break;
case "outline-style":
cssBox.OutlineStyle = value;
break;
case "outline-color":
cssBox.OutlineColor = value;
break;
case "outline-width":
cssBox.OutlineWidth = value;
break;
case "width":
cssBox.Width = value;
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Linq;
using HtmlRenderer.IntegrationTest.TestSupport;
using TheArtOfDev.HtmlRenderer.Adapters.Entities;

namespace HtmlRenderer.IntegrationTest.Painting;

/// <summary>
/// CSS 2.1 §14.2.1: <c>background-color</c> paints a solid fill behind a box's content, covering the box's
/// padding + content area (border-box minus the border itself). No dedicated background paint-call test
/// existed anywhere in this repo before this - only CSS-OM parsing (<c>BackgroundPropertyTests.cs</c>).
/// </summary>
[DoNotParallelize]
[TestClass]
public sealed class BackgroundPaintIntegrationTests
{
[TestMethod]
public void BackgroundColor_PaintsASolidFillRect()
{
var (root, container) = PaintHarness.Layout(PaintHarness.Wrap(
"<div id='b' style='width:100px;height:50px;background-color:rgb(10,20,30)'>x</div>"));
var div = PaintHarness.FindById(root, "b")!;

var g = PaintHarness.PaintBox(container, div);
var fills = g.Log.OfType<RecordingGraphics.DrawRectCall>()
.Where(r => r.Color == RColor.FromArgb(10, 20, 30)).ToList();

Assert.AreEqual(1, fills.Count);
Assert.AreEqual(100, fills[0].Width, 0.1);
Assert.AreEqual(50, fills[0].Height, 0.1);
}

[TestMethod]
public void BackgroundColorTransparent_PaintsNothing()
{
var (root, container) = PaintHarness.Layout(PaintHarness.Wrap(
"<div id='b' style='width:100px;height:50px;background-color:transparent'>x</div>"));
var div = PaintHarness.FindById(root, "b")!;

var g = PaintHarness.PaintBox(container, div);

Assert.IsFalse(g.Log.OfType<RecordingGraphics.DrawRectCall>().Any());
}

[TestMethod]
public void BackgroundColorDefault_PaintsNothing()
{
// The initial value of background-color is "transparent" - a box with no background-color set at
// all must not paint a fill either.
var (root, container) = PaintHarness.Layout(PaintHarness.Wrap(
"<div id='b' style='width:100px;height:50px;'>x</div>"));
var div = PaintHarness.FindById(root, "b")!;

var g = PaintHarness.PaintBox(container, div);

Assert.IsFalse(g.Log.OfType<RecordingGraphics.DrawRectCall>().Any());
}

[TestMethod]
public void BackgroundColor_PaintsBeforeBorderAndOutline()
{
// CSS 2.1 Appendix E: background paints before border/outline for the same box.
var (root, container) = PaintHarness.Layout(PaintHarness.Wrap(
"<div id='b' style='width:100px;height:50px;background-color:rgb(10,20,30);"
+ "border:3px solid rgb(1,2,3);outline:5px solid rgb(4,5,6)'>x</div>"));
var div = PaintHarness.FindById(root, "b")!;

var g = PaintHarness.PaintBox(container, div);

var backgroundIndex = g.Log.FindIndex(c => c is RecordingGraphics.DrawRectCall r && r.Color == RColor.FromArgb(10, 20, 30));
var borderIndex = g.Log.FindIndex(c => c is RecordingGraphics.DrawLineCall l && l.Color == RColor.FromArgb(1, 2, 3));
var outlineIndex = g.Log.FindIndex(c => c is RecordingGraphics.DrawRectCall r && r.Color == RColor.FromArgb(4, 5, 6));

Assert.IsTrue(backgroundIndex >= 0 && borderIndex >= 0 && outlineIndex >= 0);
Assert.IsTrue(backgroundIndex < borderIndex, "background must paint before the border");
Assert.IsTrue(borderIndex < outlineIndex, "border must paint before the outline");
}
}
Loading
Loading