Skip to content

Commit 62345a3

Browse files
A box round a picture fits it in a terminal too (#51)
A Frame whose body is only an unfloated Figure, laid out for a reader shown the picture, drops its padding and is as wide as the picture or its title, matching the HTML ms-box-picture. It was drawn across the whole width with the picture in its left corner. Claude-Session: https://claude.ai/code/session_01DLuNT88dr1ZgRLtgLnCHwr Co-authored-by: Claude <noreply@anthropic.com>
1 parent 51d3c51 commit 62345a3

4 files changed

Lines changed: 80 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6868
right but none above. The picture now aligns to the top of its line, and a box whose body is only a
6969
picture on a line of its own (`ms-box-picture`) has no padding and is no wider than the picture.
7070
A `Frame`'s `Padding` reaches the HTML as `--ms-pad` (it was always `1ch`).
71+
- **A box round a picture fits it in a terminal too.** A terminal that draws pictures got the box across
72+
the whole width with the picture in its left corner, so it stood wider than the rules and lines
73+
around it. A `Frame` whose body is only a picture on a line of its own, for a reader shown that
74+
picture, now has no padding and is as wide as the picture (or its title, if that is wider). A reader
75+
shown the art or the description instead still gets the box across the width.
7176

7277
## 2.11.2 — 2026-10-07
7378

‎MarkupString.Tests/Ansi/TerminalFeatureTests.cs‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,44 @@ public async Task KittyKeepsABoxAroundThePictureAligned()
191191
await Assert.That(widths).IsEquivalentTo(new[] { 20 });
192192
}
193193

194+
/// <summary>
195+
/// A box round nothing but a picture fits it, as it does in HTML: a terminal drew the box across the whole
196+
/// width with the picture in its left corner, wider than the rules and lines around it.
197+
/// </summary>
198+
[Test]
199+
public async Task ABoxRoundOnlyAPictureFitsIt()
200+
{
201+
var options = new AnsiOutputOptions(Features: TerminalFeatures.KittyGraphics) { Pictures = new Source(RedBlue()) };
202+
var laid = Laid(new Frame(new Figure(Cat, MarkupText.Empty)), 40, new PictureCells(6, 2));
203+
204+
var lines = RenderString(laid, options).Split('\n');
205+
206+
await Assert.That(lines.Select(VisibleCells).Distinct().ToArray()).IsEquivalentTo(new[] { 8 });
207+
await Assert.That(laid.ToPlainText().Split('\n')[0]).IsEqualTo("┌──────┐");
208+
}
209+
210+
/// <summary>A titled box round a picture is still as wide as its title needs.</summary>
211+
[Test]
212+
public async Task ABoxRoundAPictureKeepsRoomForItsTitle()
213+
{
214+
var laid = Laid(new Frame(new Figure(Cat, MarkupText.Empty)) { Title = MarkupText.Plain("A long title") }, 40, new PictureCells(6, 2));
215+
216+
var lines = laid.ToPlainText().Split('\n');
217+
218+
await Assert.That(lines.Select(line => line.Length).Distinct().ToArray()).IsEquivalentTo(new[] { 19 });
219+
await Assert.That(lines[0]).IsEqualTo("┌─┤ A long title ├┐");
220+
}
221+
222+
/// <summary>A reader shown the picture's text instead gets the box across the width, as with any other text.</summary>
223+
[Test]
224+
public async Task ABoxRoundAPictureNotShownKeepsItsWidth()
225+
{
226+
var laid = BlockLayout.Build(new Frame(new Figure(Cat, MarkupText.Empty)), 40,
227+
context: new LayoutContext { Pictures = (_, _) => null });
228+
229+
await Assert.That(laid.ToPlainText().Split('\n').Select(line => line.Length).Distinct().ToArray()).IsEquivalentTo(new[] { 40 });
230+
}
231+
194232
[Test]
195233
public async Task KittyWithoutThePixelsSendsTheArt()
196234
{

‎MarkupString/Layout/Blocks/Figure.cs‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,7 @@ public sealed record Figure(ImageMarkup Image, MarkupText Art) : Block
2222
/// <inheritdoc/>
2323
public override void Draw(LayoutContext context, int width, IList<MarkupText> lines)
2424
{
25-
var art = Art.Length == 0
26-
? null
27-
: Art.Split("\n").Select(line => line.Text.EndsWith('\r') ? line.Substring(0, line.Length - 1) : line).ToArray();
28-
29-
// Art of line breaks alone has no cells to hold a picture, so it is no art.
30-
if (art is not null && art.Max(line => line.DisplayWidth) == 0) art = null;
25+
var art = ArtLines();
3126

3227
// The picture is on its rows whoever reads them: a terminal that has its pixels draws it in their cells,
3328
// 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<MarkupText> li
9590
}
9691
}
9792

93+
/// <summary>The art's lines, or none: art of line breaks alone has no cells to hold a picture, so it is no art.</summary>
94+
private MarkupText[]? ArtLines()
95+
{
96+
if (Art.Length == 0) return null;
97+
var art = Art.Split("\n").Select(line => line.Text.EndsWith('\r') ? line.Substring(0, line.Length - 1) : line).ToArray();
98+
return art.Max(line => line.DisplayWidth) == 0 ? null : art;
99+
}
100+
101+
/// <summary>
102+
/// The cells wide the picture is drawn in at <paramref name="width"/> for a reader <paramref name="context"/>
103+
/// draws it for, or none when the reader gets text instead.
104+
/// </summary>
105+
internal int? PictureWidth(LayoutContext context, int width)
106+
{
107+
if (Image.Source.Length == 0 || context.Pictures is not { } pictures) return null;
108+
var artWidth = ArtLines()?.Max(line => line.DisplayWidth);
109+
return pictures(Image, Math.Min(width, artWidth ?? width)) is { Columns: > 0, Rows: > 0 } cells
110+
? Math.Min(width, artWidth ?? cells.Columns)
111+
: null;
112+
}
113+
98114
/// <summary>
99115
/// The rows of cells the picture is drawn in, each marked with the picture and its <see cref="PictureRow"/>
100116
/// over what a client the picture does not reach shows there: the art, which keeps its own size, or, with no

‎MarkupString/Layout/Blocks/Frame.cs‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,22 @@ public override void Draw(LayoutContext context, int width, IList<MarkupText> li
4949
{
5050
var style = context.Paint(context.Border(Border));
5151
var none = style.Name == BorderStyle.None.Name;
52+
var sides = style.Left.DisplayWidth + style.Right.DisplayWidth;
5253
var padding = BlockText.Blank(Padding);
53-
var inner = Math.Max(1, width - style.Left.DisplayWidth - style.Right.DisplayWidth - padding.DisplayWidth * 2);
54+
var inner = Math.Max(1, width - sides - padding.DisplayWidth * 2);
55+
56+
// A box round nothing but a picture the reader is shown fits it, as it does in HTML: no padding, and no
57+
// wider than the picture, or than the title needs.
58+
if (Body is Figure { Float: FigureFloat.None, Beside: null } figure
59+
&& figure.PictureWidth(context, Math.Max(1, width - sides)) is { } picture)
60+
{
61+
var title = Title is { Length: > 0 } t
62+
? t.DisplayWidth + style.TitleOpen.DisplayWidth + style.TitleClose.DisplayWidth + (TitleAlignment is Alignment.Left or Alignment.Right ? 2 : 1)
63+
: 0;
64+
padding = MarkupText.Empty;
65+
inner = Math.Max(1, Math.Min(width - sides, Math.Max(picture, title)));
66+
width = inner + sides;
67+
}
5468

5569
if (!none || Title is { Length: > 0 })
5670
lines.Add(BlockText.Edge(style.TopLeft, style.Top, style.TopRight, Title is null ? null : context.Paint(theme => theme.TitleColor, Title), TitleAlignment, style, width));

0 commit comments

Comments
 (0)