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));