How to create an adaptive vertical line that dynamically spans the height of its containing box/block

Hello everyone!

I am trying to create a vertical line that serves as a running rule along the content of a box/block. I am using place() to attach it to the box, but I can’t get the automatic height adjusting for the line.

My first attempt was

#set page(height: 8cm, width: 8cm, margin: 1em)

#box(
    width: 4cm,
    inset: (left: 1.4em, bottom: 1.4em, rest: 1em),
    radius: 0.4em,
    fill: blue.lighten(85%)
    )[
      #place(
        horizon + left,
        dx: -0.4em,
        // failed vertical line
        line(
          angle: 90deg,
          length: 100%,
          // start:(0%,0%), // similar approach, same result
          // end:(0%,100%)
          )
        )
      #lorem(15)
      ]

Which has output (1) in the photo:

I thought it was easily achievable via relative lengths for example (like 1fr or 100%), thinking that these lengths adapt as the content is within a container and not simply in the page.

Which they partially do, only adapting to horizontal width of a container, but not vertical. As I tested the dynamic horizontal length by placing another horizontal line as evident from (2) in the picture above.

// added the following to the content of the box
// to test horizontal dynamic length
#place(
        bottom + center,
        dy: 0.4em,
        // successful horizontal line
        line(
          angle: 0deg,
          length: 100%,
          // start:(0%,0%), // similar approach
          // end:(100%,0%)
          )
        )

My next attempt was to attempt utilizing either measure() or layout() but I can’t seem to get what I am doing wrong, maybe implementing context related ideas is a bit tricky for me.

My last attempt was to try the create everything in a context block and make a measuring function that I can access from withing the box/block but ended up failing.

#let mes(body) = context{
  let bsize = measure(body)
  [#bsize #body]
}

trying to access the value of bsize.height from within the box as it is wrapped in mes()

#mes[
  #box(
    width: 4cm,
    inset: (left: 1.4em, bottom: 1.4em, rest: 1em),
    radius: 0.4em,
    fill: blue.lighten(85%)
    )[
      #place(
        horizon + left,
        dx: -0.4em,
        // failed vertical line
        line(
          angle: 90deg,
          length: 100%, // cannot use `#bsize.height`
          // start:(0%,0%), // similar approach
          // end:(0%,100%)
          )
        )
      #place(
        bottom + center,
        dy: 0.4em,
        // successful horizontal line
        line(
          angle: 0deg,
          length: 100%,
          // start:(0%,0%), // similar approach
          // end:(100%,0%)
          )
        )
      #lorem(15)
      ]
]

Which gave:

I would like a way to make the vertical line adapt to the size of the container.

I apologize in advance if I am missing something obvious.

Should the line extend to the edges of the blue box? If it’s ok to only follow the content itself, this is an option:

#let distanceToLine = 0.2em

#box(
  width: 4cm,
  inset: (left: 1.4em - distanceToLine, bottom: 1.4em, rest: 1em),
  radius: 0.4em,
  fill: blue.lighten(85%),

  box(
    inset: (left: distanceToLine, rest: 0pt),
    stroke: (left: 1pt),
    lorem(15)
  )
)

image

1 Like

I just played around with this a bit, and it seems that your original example does work when you explicitly specify the height of the box.

So maybe this is actually a bug where typst does not correctly resolve the box height and falls back to the height of the outer container (the page in this case)?

2 Likes

Thanks for your input, it is effective and less cluttery, and avoids the current possible blind spot of Typst’s automatic height resizing.

but as I played around with the proposed solution…

  • I found that adjusting the spacing/padding between the line and and the text is tricky. I figured out how to change it in hacky way, by changing the outset: (left: length) of the most inner box, which works, but feels inconvenient modifying the in(out)sets of nested boxes.
  • I was looking for an extendable solution to tackle sizing the height of lines (as in #line()), or potentially any similar object I can attach to a container, as actual lines or shapes can be combined and be more customizable.
    For example I was actually working towards something like this:
    pic
    (achieved similar results using LaTeX, using concepts like frame.northwest for top left coordinate)

best I could achieve in Typst so far was:
Typst_best

using the following code:

#set  page(width: 10cm, height: 10cm)
#set text(size:13pt)

#let distanceToLine = 0.2em
#box(
  width: 4cm,
  inset: (left: 1.4em - distanceToLine, bottom: 1.4em, rest: 1em),
  radius: 0.4em,
  fill: blue.lighten(85%),

  box(
    inset: (left: distanceToLine, rest: 0pt),
    outset: (left:0.5em),  // to nudge the line 
    stroke: (left: (
            thickness: 0.3em,
            paint: blue,
            cap: "round",  // not applying? 
            )
            ),
    lorem(10)
  )
)

I am trying to learn the software, and such cases push me to try understand how to implement a solution using the native tools.

It is weird that auto-adjust v-length doesn’t work consistently as auto-adjusting h-length inside containers. But if it boils down to a hack or a package (maybe there is a packages that tackles this technicality?) that’s still a win.

1 Like

Oh good find!, I confirmed it,

and it seems like the height (unless specified) is treated like an open-ended quantity; as you can always add/remove more text, hence changing the height constantly, unlike the width which is either page’s width or specified explicitly.

whether this is intentional or not, we’d love to have a way to choose one behavior over the other.

One way this can be bypassed, is to add an option to functions like #line() to explicitly calculate the height of its container rather than falling back to the page’s height.

Or introduce more location tools like a way to query for frame.topleft.

There is no difference between height and width I don’t think. Your original box - in the first post has a fixed width of 4cm, and that’s why the sizing was ok for the horizontal line and not the vertical line. In the same circumstances (no fixed width/height) horizontal and vertical lines of 100% length behave the same way, which is, filling to page size minus margins.

1 Like

Here’s a demo of layout + measure which can sometimes be used to measure size of a part of the document and adapt size of things - like the length of the line. I think it works well in this case. In this case though I would prefer to use block border because it’s a simpler solution. But there are other examples where one has to use layout/measure.

This takes the code from the first post and adapts it to use layout/measure

#set page(height: 8cm, width: 8cm, margin: 1em)

/// Call with content to be laid out and a function that will draw the line and the content
/// the function receives a (width, height) dictionary from layout as its only argument
#let size-my-line(content, line-func) = {
  layout(layout-size => {
    let laid-out-size = measure(content, ..layout-size)
    line-func(laid-out-size)
  })
}

#box(
    width: 4cm,
    inset: (left: 1.4em, bottom: 1.4em, rest: 1em),
    radius: 0.4em,
    fill: blue.lighten(85%)
    )[
      #let the-text = lorem(15)
      #size-my-line(the-text, size => {
        place(
          horizon + left,
          dx: -0.4em,
          line(
            angle: 90deg,
            length: size.height,
            )
          )
        the-text
        // alternate way (with slightly different spacing by default)
        /*stack(dir: ltr, spacing: 0.4em)[
          #line(angle: 90deg, length: size.height)
        ][#the-text]*/
      })
    ]

bild

And yes - this solution is still not something that measures the box itself. It just measures the text we want to layout inside the current layout context - remember that’s a box with fixed width 4cm. The layout computation produces a real height as a product of the context with fixed width. Then we size the line according to the text.

Typst app hover picture: this is the input to the layout, the space available. Note how all the page height minus margin is available more or less.

bild

2 Likes

Here is the styling:

#let custom-line(stroke) = line(length: 100%, angle: 90deg, stroke: stroke)
#let custom-block = block.with(
  width: 8cm,
  height: 5.1cm,
  inset: (left: 2.5em, rest: 1em),
  radius: 1em,
  fill: blue.transparentize(70%),
)

#let block1(body) = custom-block({
  let line-blue = custom-line((cap: "round", thickness: 5pt, paint: blue))
  place(dx: -1.25em, line-blue)
  body
})

#let block2(body) = custom-block({
  let stroke = (cap: "round", thickness: 5pt, paint: red)
  let line-red-with-square = grid(
    rows: (1fr, auto, 1fr),
    row-gutter: 10pt,
    align: center,
    custom-line(stroke),
    square(size: 5pt, stroke: stroke, radius: 3pt),
    custom-line(stroke),
  )
  place(dx: -1.25em - 2.5pt, line-red-with-square)
  body
})

#set par(justify: true)

#block1[
  #lorem(52)
]

#block2[
  #lorem(52)
]

4 Likes

Thank you for the insight, you are right, I stand corrected.

Playing with containers that have an automatic width yields the same result when creating a 100% long line within them!

I admit that, the correct statement was not that it was inconsistent, but rather, it felt unnatural albeit fair and consistent.

Brilliant solution, thank you so much. for your time and effort.

I was fixated on solving things in terms of the box, and not the height of its content.

just to make sure I benefited from your answer…
If I understand correctly, the line-func is a place holder for a function-type input (to abstract the object that will benefit from the size measurement?) for when we called the #size-my-line actually inside the box?

Beautiful work, thanks a lot. I managed to cobble something for the first style, but didn’t know whether to use stack or grid for the compound red line.

I didn’t know about the stroke issue you pointed out on GitHub. thank you for your efforts.

1 Like

Not sure how to explain this best, but line-func is a callback to get the computed size value. We can’t extract this value any other way, can’t return it out of the layout function, so we use a callback. So anything that uses the size value needs to be drawn inside the callback I think.

1 Like

You can use whatever, just need to weigh all pros and cons. I started with stack, because it looks like the perfect contender, and the default dir is already correct, but when I realized the alignment problem (and also the 100% x2), I switched to grid. And because of the stroke not being counted into any bounding box, you need to compensate for half of its thickness in place(). You can automate a lot of it, so you don’t have to manually calculate everything.

Also, a good tool for working with stroke is

#import "@preview/t4t:0.4.2": get
#get.stroke-dict(red + 5pt)

image

But since you’ve mentioned a native way, I didn’t use it.

1 Like