diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75752d5..7f98501 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -68,6 +68,11 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
right but none above. The picture now aligns to the top of its line, and a box whose body is only a
picture on a line of its own (`ms-box-picture`) has no padding and is no wider than the picture.
A `Frame`'s `Padding` reaches the HTML as `--ms-pad` (it was always `1ch`).
+- **A box round a picture fits it in a terminal too.** A terminal that draws pictures got the box across
+ the whole width with the picture in its left corner, so it stood wider than the rules and lines
+ around it. A `Frame` whose body is only a picture on a line of its own, for a reader shown that
+ picture, now has no padding and is as wide as the picture (or its title, if that is wider). A reader
+ shown the art or the description instead still gets the box across the width.
## 2.11.2 — 2026-10-07
diff --git a/MarkupString.Tests/Ansi/TerminalFeatureTests.cs b/MarkupString.Tests/Ansi/TerminalFeatureTests.cs
index ca268e6..d468eb6 100644
--- a/MarkupString.Tests/Ansi/TerminalFeatureTests.cs
+++ b/MarkupString.Tests/Ansi/TerminalFeatureTests.cs
@@ -191,6 +191,44 @@ public async Task KittyKeepsABoxAroundThePictureAligned()
await Assert.That(widths).IsEquivalentTo(new[] { 20 });
}
+ ///
+ /// A box round nothing but a picture fits it, as it does in HTML: a terminal drew the box across the whole
+ /// width with the picture in its left corner, wider than the rules and lines around it.
+ ///
+ [Test]
+ public async Task ABoxRoundOnlyAPictureFitsIt()
+ {
+ var options = new AnsiOutputOptions(Features: TerminalFeatures.KittyGraphics) { Pictures = new Source(RedBlue()) };
+ var laid = Laid(new Frame(new Figure(Cat, MarkupText.Empty)), 40, new PictureCells(6, 2));
+
+ var lines = RenderString(laid, options).Split('\n');
+
+ await Assert.That(lines.Select(VisibleCells).Distinct().ToArray()).IsEquivalentTo(new[] { 8 });
+ await Assert.That(laid.ToPlainText().Split('\n')[0]).IsEqualTo("┌──────┐");
+ }
+
+ /// A titled box round a picture is still as wide as its title needs.
+ [Test]
+ public async Task ABoxRoundAPictureKeepsRoomForItsTitle()
+ {
+ var laid = Laid(new Frame(new Figure(Cat, MarkupText.Empty)) { Title = MarkupText.Plain("A long title") }, 40, new PictureCells(6, 2));
+
+ var lines = laid.ToPlainText().Split('\n');
+
+ await Assert.That(lines.Select(line => line.Length).Distinct().ToArray()).IsEquivalentTo(new[] { 19 });
+ await Assert.That(lines[0]).IsEqualTo("┌─┤ A long title ├┐");
+ }
+
+ /// A reader shown the picture's text instead gets the box across the width, as with any other text.
+ [Test]
+ public async Task ABoxRoundAPictureNotShownKeepsItsWidth()
+ {
+ var laid = BlockLayout.Build(new Frame(new Figure(Cat, MarkupText.Empty)), 40,
+ context: new LayoutContext { Pictures = (_, _) => null });
+
+ await Assert.That(laid.ToPlainText().Split('\n').Select(line => line.Length).Distinct().ToArray()).IsEquivalentTo(new[] { 40 });
+ }
+
[Test]
public async Task KittyWithoutThePixelsSendsTheArt()
{
diff --git a/MarkupString/Layout/Blocks/Figure.cs b/MarkupString/Layout/Blocks/Figure.cs
index cf3ea02..b18fddb 100644
--- a/MarkupString/Layout/Blocks/Figure.cs
+++ b/MarkupString/Layout/Blocks/Figure.cs
@@ -22,12 +22,7 @@ public sealed record Figure(ImageMarkup Image, MarkupText Art) : Block
///
public override void Draw(LayoutContext context, int width, IList lines)
{
- var art = Art.Length == 0
- ? null
- : Art.Split("\n").Select(line => line.Text.EndsWith('\r') ? line.Substring(0, line.Length - 1) : line).ToArray();
-
- // Art of line breaks alone has no cells to hold a picture, so it is no art.
- if (art is not null && art.Max(line => line.DisplayWidth) == 0) art = null;
+ var art = ArtLines();
// The picture is on its rows whoever reads them: a terminal that has its pixels draws it in their cells,
// MXP or Pueblo writes it once, and everything else reads what the rows hold.
@@ -95,6 +90,27 @@ public override void Draw(LayoutContext context, int width, IList li
}
}
+ /// The art's lines, or none: art of line breaks alone has no cells to hold a picture, so it is no art.
+ private MarkupText[]? ArtLines()
+ {
+ if (Art.Length == 0) return null;
+ var art = Art.Split("\n").Select(line => line.Text.EndsWith('\r') ? line.Substring(0, line.Length - 1) : line).ToArray();
+ return art.Max(line => line.DisplayWidth) == 0 ? null : art;
+ }
+
+ ///
+ /// The cells wide the picture is drawn in at for a reader
+ /// draws it for, or none when the reader gets text instead.
+ ///
+ internal int? PictureWidth(LayoutContext context, int width)
+ {
+ if (Image.Source.Length == 0 || context.Pictures is not { } pictures) return null;
+ var artWidth = ArtLines()?.Max(line => line.DisplayWidth);
+ return pictures(Image, Math.Min(width, artWidth ?? width)) is { Columns: > 0, Rows: > 0 } cells
+ ? Math.Min(width, artWidth ?? cells.Columns)
+ : null;
+ }
+
///
/// The rows of cells the picture is drawn in, each marked with the picture and its
/// over what a client the picture does not reach shows there: the art, which keeps its own size, or, with no
diff --git a/MarkupString/Layout/Blocks/Frame.cs b/MarkupString/Layout/Blocks/Frame.cs
index 59355a8..a73f7d0 100644
--- a/MarkupString/Layout/Blocks/Frame.cs
+++ b/MarkupString/Layout/Blocks/Frame.cs
@@ -49,8 +49,22 @@ public override void Draw(LayoutContext context, int width, IList li
{
var style = context.Paint(context.Border(Border));
var none = style.Name == BorderStyle.None.Name;
+ var sides = style.Left.DisplayWidth + style.Right.DisplayWidth;
var padding = BlockText.Blank(Padding);
- var inner = Math.Max(1, width - style.Left.DisplayWidth - style.Right.DisplayWidth - padding.DisplayWidth * 2);
+ var inner = Math.Max(1, width - sides - padding.DisplayWidth * 2);
+
+ // A box round nothing but a picture the reader is shown fits it, as it does in HTML: no padding, and no
+ // wider than the picture, or than the title needs.
+ if (Body is Figure { Float: FigureFloat.None, Beside: null } figure
+ && figure.PictureWidth(context, Math.Max(1, width - sides)) is { } picture)
+ {
+ var title = Title is { Length: > 0 } t
+ ? t.DisplayWidth + style.TitleOpen.DisplayWidth + style.TitleClose.DisplayWidth + (TitleAlignment is Alignment.Left or Alignment.Right ? 2 : 1)
+ : 0;
+ padding = MarkupText.Empty;
+ inner = Math.Max(1, Math.Min(width - sides, Math.Max(picture, title)));
+ width = inner + sides;
+ }
if (!none || Title is { Length: > 0 })
lines.Add(BlockText.Edge(style.TopLeft, style.Top, style.TopRight, Title is null ? null : context.Paint(theme => theme.TitleColor, Title), TitleAlignment, style, width));