From ab3ccfccea9284b0999617fb23d2617ec0e884d5 Mon Sep 17 00:00:00 2001 From: Daniel Romell Date: Tue, 18 Aug 2026 10:30:06 +0200 Subject: [PATCH] feat: add description-width config option Descriptions and values are drawn as a single line that never wraps. Because a diagram is scaled to fit the width it is rendered into, the longest label is what decides that scale: one long description makes the whole diagram -- bit cells, indices and every other label with it -- shrink until that line fits. A register whose fields carry a sentence each is then unreadable at any page width, and since each diagram is measured on its own, a document full of them renders every one at a different size. Setting description-width wraps labels at that width instead, so the length of a description no longer decides how large the diagram is drawn. Values wrap at the same right edge as the description they belong to, and the option composes with left-labels, force-descs-on-side and ltr-bits. Wrapping means a label is no longer one line tall, so labels can no longer be spaced by a fixed line height. Each is now offset by the height it actually occupies, measured before it is placed, which is why render gained a context block. The option defaults to none, which keeps the single-line behaviour on the original code paths. With it unset every diagram in the gallery renders byte-identical to before. --- CHANGELOG.md | 3 ++ docs/config.typ | 4 ++- manual.typ | 30 ++++++++++++++++ src/config.typ | 2 ++ src/renderer.typ | 90 ++++++++++++++++++++++++++++++++++++++---------- 5 files changed, 110 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc083ae..15c5593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [Unreleased] +- Added `description-width` config option, wrapping descriptions and values instead of drawing them on a single line + ## [v0.3.1] - 2026-06-14 - Support for non-consecutive ranges ([#13](https://git.kb28.ch/HEL/rivet-typst/pulls/13)) - Prevent drawing separators on starting lines of fields ([#17](https://git.kb28.ch/HEL/rivet-typst/pulls/17)) diff --git a/docs/config.typ b/docs/config.typ index d3c3ed0..3d1f991 100644 --- a/docs/config.typ +++ b/docs/config.typ @@ -26,6 +26,7 @@ /// - full-page (bool): If true, the page will be resized to fit the diagram and take the background color /// - all-bit-i (bool): If true, all bit indices will be rendered, otherwise, only the ends of each range will be displayed /// - ltr-bits (bool): If true, bits are placed with the LSB on the left instead of the right +/// - description-width (float | none): If set, descriptions and values wrap at this width instead of being drawn on a single line /// -> dictionary #let config( default-font-family: "Ubuntu Mono", @@ -53,7 +54,8 @@ height: 800, full-page: false, all-bit-i: true, - ltr-bits: false + ltr-bits: false, + description-width: none ) = {} /// Dark theme config diff --git a/manual.typ b/manual.typ index 65d75d0..2f0df16 100644 --- a/manual.typ +++ b/manual.typ @@ -203,6 +203,36 @@ For values depending on other ranges, see #link()[Dependenc } ``` +=== Wrapping descriptions + +Descriptions and values are drawn on a single line. A diagram is scaled to fit +the width it is given, so a long description makes the whole diagram, bit cells +and all, shrink until it fits: + +#let long-schema = (structures: (main: (bits: 16, ranges: ( + "15-8": (name: "op", description: "Operation to perform on the two operands"), + "7-0": ( + name: "flags", + description: "Flags applied to the operation, see the table below for the meaning of each bit", + values: ("00000001": "carry in, added to the result before the flags are evaluated") + ) +)))) +#let long-sch = schema.load(long-schema) + +#align(center, schema.render(long-sch, width: 75%)) + +Setting the #doc-ref("config.config") option `description-width` wraps them at +that width instead, leaving the diagram at its natural size. The width is given +in the same units as `bit-width`, and values wrap at the same right edge as the +description they belong to. Each label is offset by the height it actually +occupies, so wrapping onto several lines does not make labels overlap. + +```typ +schema.render(sch, config: config.config(description-width: 300)) +``` + +#align(center, schema.render(long-sch, width: 75%, config: lib.config.config(description-width: 300))) + #pagebreak(weak: true) == Dependencies diff --git a/src/config.typ b/src/config.typ index 5b7fac0..df837dd 100644 --- a/src/config.typ +++ b/src/config.typ @@ -25,6 +25,7 @@ full-page: false, all-bit-i: true, ltr-bits: false, + description-width: none, ) = { return ( default-font-family: default-font-family, @@ -53,6 +54,7 @@ full-page: full-page, all-bit-i: all-bit-i, ltr-bits: ltr-bits, + description-width: description-width, ) } diff --git a/src/renderer.typ b/src/renderer.typ index ec0c3d3..6070c3b 100644 --- a/src/renderer.typ +++ b/src/renderer.typ @@ -41,6 +41,24 @@ ) } +// A wrapped label is built as plain content so that its height can be measured +// before it is placed, and the label after it offset by what it really takes. +#let text-block(txt, color, width, font: none, italic: false, size: 1em) = { + let text-params = (:) + if font != none { + text-params.insert("font", font) + } + if italic { + text-params.insert("style", "italic") + } + + box(width: width * 1pt, text(txt, fill: color, size: size, ..text-params)) +} + +#let draw-block(block, x, y) = { + draw.content((x, -y), block, anchor: "north-west", stroke: none) +} + #let draw-line(color, a, b) = { let (x0, y0) = a let (x1, y1) = b @@ -113,15 +131,26 @@ for (val, desc) in values.pairs().sorted(key: p => p.first()) { desc-y += gap let txt = val + " = " + desc - shapes += draw-text( - txt, txt-col, desc-x + bit-w / 2, desc-y, - anchor: "north-west", - font: config.italic-font-family, - italic: true, - size: config.italic-font-size - ) + if config.description-width == none { + shapes += draw-text( + txt, txt-col, desc-x + bit-w / 2, desc-y, + anchor: "north-west", + font: config.italic-font-family, + italic: true, + size: config.italic-font-size + ) - desc-y += config.italic-font-size / 1.2pt + desc-y += config.italic-font-size / 1.2pt + } else { + let block = text-block( + txt, txt-col, config.description-width - bit-w / 2, + font: config.italic-font-family, + italic: true, + size: config.italic-font-size + ) + shapes += draw-block(block, desc-x + bit-w / 2, desc-y) + desc-y += measure(block).height.pt() + } } return (shapes, desc-x, desc-y) @@ -149,22 +178,47 @@ shapes += draw-underbracket(config, start-x, start-x + width, start-y) let mid-x = start-x + width / 2 - shapes += draw-link(config, mid-x, start-y, desc-x, desc-y) + let link-y = if config.description-width == none { + desc-y + } else { + // wrapped labels hang from desc-y, so aim at the middle of their first line + desc-y + config.default-font-size.pt() / 2 - bit-h / 2 + } + shapes += draw-link(config, mid-x, start-y, desc-x, link-y) let txt-x = desc-x if config.left-labels { - txt-x -= range_.description.len() * config.default-font-size / 2pt + // desc-x is the right edge of the label when they are on the left, so shift + // to its left edge: exactly the box width when wrapping, an estimate from + // the character count otherwise + txt-x -= if config.description-width == none { + range_.description.len() * config.default-font-size / 2pt + } else { + config.description-width + } } - shapes += draw-text( - range_.description, - config.text-color, - txt-x, desc-y + bit-h / 2, - anchor: "west" - ) + if config.description-width == none { + shapes += draw-text( + range_.description, + config.text-color, + txt-x, desc-y + bit-h / 2, + anchor: "west" + ) - desc-y += config.default-font-size / 0.75pt + desc-y += config.default-font-size / 0.75pt + } else { + let block = text-block( + range_.description, + config.text-color, + config.description-width, + font: config.default-font-family, + size: config.default-font-size + ) + shapes += draw-block(block, txt-x, desc-y) + desc-y += measure(block).height.pt() + } if range_.values != none and range_.depends-on == none { let shapes_ @@ -519,7 +573,7 @@ return (shapes, desc-y) } -#let render(config, schema, width: 100%) = { +#let render(config, schema, width: 100%) = context { set text( font: config.default-font-family, size: config.default-font-size