From 2dcbf77d723b83257d46507d82a68ec3153d41c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 04:06:04 +0000 Subject: [PATCH] A box round a picture spans the width in a terminal again 2.14.2 (#51) fitted a box holding only a picture to the picture. In a scene log that box stood narrower than every other pose's box and rule. It is drawn across the width again, with its padding, and the picture at its left. HTML keeps fitting it (ms-box-picture). This reverts the Frame and Figure parts of 62345a3. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_018ztDNVgzxM8V5WKWBRs2EW --- CHANGELOG.md | 9 +++--- .../Ansi/TerminalFeatureTests.cs | 32 +++---------------- MarkupString/Layout/Blocks/Figure.cs | 28 ++++------------ MarkupString/Layout/Blocks/Frame.cs | 16 +--------- 4 files changed, 16 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99350b9..b62d161 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,10 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed +- **A box round a picture spans the width in a terminal again.** 2.14.2 fitted a box holding only a + picture to the picture, so in a scene log it stood narrower than every box and rule around it. It is + drawn across the width again, with its padding, and the picture at its left. HTML still fits it + (`ms-box-picture`). - **An MXP client draws a figure's picture in its cells.** A figure's `` carried no size, so a client drew the picture at its own size under the line it was named on, outside any box round it. It is now sized in the cells laid out for it (`W=12c H=5c`), the art's or `PictureCells`', and a client @@ -73,11 +77,6 @@ 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 d468eb6..fd3888f 100644 --- a/MarkupString.Tests/Ansi/TerminalFeatureTests.cs +++ b/MarkupString.Tests/Ansi/TerminalFeatureTests.cs @@ -192,41 +192,19 @@ public async Task KittyKeepsABoxAroundThePictureAligned() } /// - /// 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. + /// A box round nothing but a picture spans the width, as a box round text does, so it lines up with the boxes + /// and rules around it; the picture sits at its left. /// [Test] - public async Task ABoxRoundOnlyAPictureFitsIt() + public async Task ABoxRoundOnlyAPictureSpansTheWidth() { 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 }); + await Assert.That(lines.Select(VisibleCells).Distinct().ToArray()).IsEquivalentTo(new[] { 40 }); + await Assert.That(laid.ToPlainText().Split('\n')[0]).IsEqualTo("┌" + new string('─', 38) + "┐"); } [Test] diff --git a/MarkupString/Layout/Blocks/Figure.cs b/MarkupString/Layout/Blocks/Figure.cs index 2d8127e..72b53d7 100644 --- a/MarkupString/Layout/Blocks/Figure.cs +++ b/MarkupString/Layout/Blocks/Figure.cs @@ -22,7 +22,12 @@ public sealed record Figure(ImageMarkup Image, MarkupText Art) : Block /// public override void Draw(LayoutContext context, int width, IList lines) { - var art = ArtLines(); + 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; // 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. @@ -90,27 +95,6 @@ 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 a73f7d0..59355a8 100644 --- a/MarkupString/Layout/Blocks/Frame.cs +++ b/MarkupString/Layout/Blocks/Frame.cs @@ -49,22 +49,8 @@ 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 - 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; - } + var inner = Math.Max(1, width - style.Left.DisplayWidth - style.Right.DisplayWidth - padding.DisplayWidth * 2); 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));