How can I get a box to use all height under a floating element?

I want to offer two functions:

  • floating(it): places it as a floating element at the top of the page (plus some other stuff irrelevant to this question)
  • tall-box(it): wraps it in a box that uses the full height, minus the height of the floating element if present

My idea was to have floating add metadata with the float height, and have tall-box retrieve the metadata to size the box correctly.

But that doesn’t seem to work:

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

#let float-label() = label("float" + str(here().page()))

#let floating(it) = context {
  let height = measure(it).height
  let new-it = [#it #metadata(height) #float-label()]
  place(top, float: true, clearance: 0pt, new-it)
}

#let tall-box(color, it) = context {
  let floats = query(float-label())
  let float-h = if floats.len() == 0 { 0pt } else { floats.first().value }
  context box(fill: color, height: 100% - float-h, it)
}

#set align(horizon)

// page with tall box using the full height
#box(fill: red, inset: 5mm)[User content]
#tall-box(green)[Tall box]

#pagebreak(weak: true)

// page with blue float, and tall box using remaining height
#floating(box(fill: blue, inset: 5mm, [Floating element]))
#box(fill: red, inset: 5mm)[User content]
#tall-box(green)[Tall box]

The tall box gets pushed to a third page. I guess on the first iteration the float is not found so the tall box is sized too high, then the float is placed and the tall box is pushed to the next page. How can I avoid that?

Weirdly it seems to work if I put the #floating line last:

...
#box(fill: red, inset: 5mm)[User content]
#tall-box(green)[Tall box]
#floating(box(fill: blue, inset: 5mm, [Floating element]))

image

but in my application floating will usually be called first so I can’t rely on that.

When compiling it with 2024-09-09 version, it gives a warning about not being able to converge in 5 tries.

I think it is because floating figures will be inserted last, after everything else is typeset, and it is clear where it is better to put it. But tall-box depends on the floating position, and it should be placed first. So there is some time-related conflict.

I don’t really know how adding float last fixes this, but it somehow does. Either there is some hidden behavior that I don’t know, or it’s some sort of bug.

The solution is very clever, and I personally don’t know how to change it.

I assume that this is because it places some other stuff at the top too?

Thanks, I just tried with the current main and don’t see any convergence issue so something must have changed recently.

Regarding what you say about floats being inserted last: at first I thought that cannot be it because here with place(top, float: true, ...) the float position is already given. But maybe the implementation is shared with other types of floats that don’t have fix position, so indeed that could explain it.

That can happen, though the typical use for this floating function would be a show rule for some headings, and the red and green boxes correspond to content that the user adds later in the document.

Edit: thinking more about it, another reason why the float insertion could be delayed is because typst might decide to insert first another float (this one without fixed alignment.

I’m not 100% sure, but @laurmaedje is working on New flow layout, with multi-column floats by laurmaedje · Pull Request #5017 · typst/typst · GitHub, which might introduce enough changes so that this question can be solved more easily. One of the linked issues might be helpful too: Support fraction as block height · Issue #4220 · typst/typst · GitHub.

1 Like

Haha I reported that linked issue :) Indeed now that this awesome PR fixed it I can use block(height: 1fr, ...) which covers the main use case.

It’s not a complete fix since I also have the red box on the left, so I must get the user to put red and green together in a block(height: 1fr). It’s also a bit less flexible than what I had in mind: I thought tall-box could have a parameter to make the height only 90% or 50% or whatever, but height: 1fr will always take the full height.

Anyway I think this will be good enough for now.

You can try 0.5fr, but I think it won’t make a difference since it’s the only block that has a fraction height.

Yeah exactly… Also I’d like to be able to reserve some space on the page for other things above or below, e.g. having a tall-box height of 100% - 2cm. I can convert that to the correct height if I know the height of the floating element (though it doesn’t work as discussed above because the floating element is found too late). With a ...fr height I cannot reserve space.

Doesn’t this work how you want it?

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

#let floating = place.with(top, float: true, clearance: 0pt)
#let tall-box(color, height: 100%, it) = {
  box(fill: color, height: height, align(horizon, it))
}

// page with tall box using the full height
#box(fill: red, inset: 5mm)[User content]
#tall-box(green)[Tall box]

#pagebreak()

// page with blue float, and tall box using remaining height
#floating(box(fill: blue, inset: 5mm, [Floating element]))
#block(height: 1fr, {
  set align(bottom)
  box(fill: red, inset: 5mm)[User content]
  tall-box(green, height: 50%)[Tall box]
})

Since box.height already supports relative type, you can do that.

#block(height: 1fr, {
  set align(bottom)
  box(fill: red, inset: 5mm)[User content]
  tall-box(green, height: 100% - 2cm)[Tall box]
})

There is also v(1fr) that you can integrate somewhere, maybe.

1 Like

Yes that works! I got to the same solution :) The downside is that I must ask the user to add this block(height: 1fr, it) wrapper themselves. I was hoping I could get floating and tall-box to just work. For now I’ll add the block wrapper method to the documentation.

So I finally found a solution that checks all the boxes, though I’m not sure how much I can rely on it working with future typst versions:

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

#let float-label(page) = label("float" + str(page))

#let floating(it) = context {
  let height = measure(it).height
  let new-it = [#it #metadata(height) #float-label(here().page())]
  place(top, float: true, clearance: 0pt, new-it)
}

#let tall-box(color, it) = {
  context [#metadata((page: here().page()))<tall-box-pos>]
  context {
    let md = query(selector(<tall-box-pos>).before(here())).last()
    let floats = query(float-label(md.value.page))
    let float-h = if floats.len() == 0 { 0pt } else { floats.first().value }
    context box(fill: color, height: 100% - float-h, it)
  }
}

#set align(horizon)

// page with tall box using the full height
#box(fill: red, inset: 5mm)[User content]
#tall-box(green)[Tall box]
#pagebreak(weak: true)

// page with blue float, and tall box using remaining height
#floating(box(fill: blue, inset: 5mm, [Floating element]))
#box(fill: red, inset: 5mm)[User content]
#tall-box(green)[Tall box]