Why do I need state for some (here some specific) show rules?

As a follow up to: How to pagebreak before an heading, only if a certain condition is achieved?

objective: make headings appear on a new page if they would otherwise be placed in the bottom half of a page.

Here again the two versions: they look pretty similar, but option 1 doesn’t work and option 2 does:

option 1
// option 1
#show heading.where(level: 1, outlined: true): it => {
  context if here().position().y > page.height * threshold {
    pagebreak()
  }
  it
}
option 2
// option 2
#let my-location = state("my-location", (:))

#show heading.where(level: 1, outlined: true): it => {
  context my-location.update(here().position())
  context {
    if my-location.get().y > page.height * (100% - threshold) {
      pagebreak()
    }
  }
  it
}

editable example: Typst

1 Like

The problem in option 1 is that the layout doesn’t converge. Typst makes multiple layout iterations until the layout “converges”, meaning until it doesn’t change anymore.

For your example: In the first run, the y position will be zero because Typst doesn’t know yet where the heading will be placed, so no page break will be inserted. So after the first run the headings are placed “as usual”. Then it goes into the second run. Now the position will give you the “usual position”. So it detects the bad headings and inserts the appropriate page breaks. The result of this is basically the document that you want. However, the layout looks different from the previous iteration (i.e. didn’t converge yet), so we go into a third run. Now for third run, it return as positions the new positions (how you want them to be). That means no heading will be detected as a bad heading, and hence no page breaks will be inserted, or in other words, the page breaks will be removed again. So what gets compiled is the document from the start! In this fashion it will switch infinitely back and forth between the “usual” document and the document that you want. Hence a compiler error that the layout doesn’t converge.

In your second option you managed (probably more by luck if I may say so) to bypass this problem. You can go through the different iterations by hand and check that doesn’t lead to the infinite back-and-forth. In fact, if I’m not mistaken, that’s because of a funny “back-and-forth” between the state and the iterations. The state is basically always the opposite of what you want it to be. Whatever.

Let me mention another point with which you have to be careful when implementing this. If your implementation makes the fix say at the second layout iteration, that could be too early. Because the content could still change (because of many other context calls). That means it could still happen that an already-fixed heading will be turned bad again because say in the third iteration suddenly a lot of text appears before the heading. This makes implementing the behavior you want a little challenging, good luck. :smiley:

3 Likes

Thank you sooo much. That explains a lot.

Regarding this topic the answer then is: in the example given, there is actually no evidence that using of state is needed or provides more possibilities since both options don’t do the trick.

That is unfortunate in general xD

At least it sheds some light on the hidden mechanics of typst. (There are still a lot of questions about how all these show rules work. And which are clearly nowhere stated. I’m a little sad that it is currently all just figuring out by trial and error. - and nice posts by people like @jbirnick . But maybe that’s because the dev don’t want to drop too much info about this stuff because they are subject to change?)

Yeah the state of the docs is not optimal, the way I learned many things is by asking on the Discord. But it will improve as Typst evolves I guess. It’s a lot of work and not easy to write extensive docs.

Btw this should be a solution to your problem, thanks @quachpas for the metadata inspiration:

#show heading: it => {
  metadata("originalheading")
  context {
    let m = query(selector(metadata.where(value: "originalheading")).before(here())).last()
    if m.location().position().y > 0.5 * page.height {
      pagebreak()
    }
  }
  it
}

It puts some metadata at the original position of the heading (that’s the important part), and then makes a decision based on that, original, position.

1 Like

Nice. No I’m a little disappointed in myself. I tried to do the exact same as you did. Also with inspiration of @quachpas idea. However I didn’t manage to produce a satisfactory solution. I must have missed something …

Again: thank you so much :)

1 Like

@jbirnick btw you can discard the selector() call. .where() returns a selector already :)

1 Like

To finish this topic:
The provided answer by jbirnick also is not sufficient as stated here: How to pagebreak before an heading, only if a certain condition is achieved? - #10 by sjfhsjfh

For more information visit the other topic :) (unfortunately the practical question seems not to be solveable with the current framework)