There are likely many different ways to achieve something like that.
One option would be to use a show rule on outline.entry
like you suggested, and using a state to keep track of the widths for the number and page columns. The downside here is that you would need to use different states if you have multiple outlines (e.g. for a list of figures), or else they would all have the same column widths. This is of course possible by using counters to count the outlines and then using the corresponding counter value in the name of the state, but it would still introduce quite a bit of complexity.
Code Example
#let outline-state = state("outline", (number-width: 0pt, page-width: 0pt))
#show outline.entry: it => context {
let body = if it.element.func() == heading { it.element.body }
else { it.element.caption.body }
let number = if it.element.numbering != none {
let counter = if it.element.has("counter") { it.element.counter }
else { counter(heading) }
numbering(
it.element.numbering,
..counter.at(it.element.location())
)
}
let page-numbering = it.element.location().page-numbering()
if page-numbering == none { page-numbering = "1" }
let page = numbering(
page-numbering,
..counter(page).at(it.element.location())
)
// Update state with current number/page widths.
let number-width = measure(number).width
let page-width = measure(page).width
outline-state.update(s => (
number-width: calc.max(number-width, s.number-width),
page-width: calc.max(page-width, s.page-width)
))
// Get final width values and construct outline entry.
let (number-width, page-width) = outline-state.final()
box(grid(
column-gutter: 0.5em,
columns: (number-width, 1fr, page-width),
align: (start, start, end + bottom),
number, body + [ ] + box(width: 1fr, it.fill), page
))
}
Another shorter option would be to use a show rule on outline
. There, you can then query the elements manually and directly lay them out in a grid with three columns. The downside here is that you can’t nicely reconstruct the automatically translated outline title.
Code Example
#show outline: it => {
let elements = query(it.target)
let has-number = elements.any(el => el.numbering != none)
// Can't use the automatically translated title here.
heading(if it.title == auto [ Contents ] else { it.title })
grid(
column-gutter: 0.5em,
align: (start, start, end + bottom),
row-gutter: par.leading,
columns: (auto, 1fr, auto),
..elements.map(el => {
let body = if el.func() == heading { el.body } else { el.caption.body }
let num = if el.numbering != none {
let counter = if el.has("counter") { el.counter } else { counter(heading) }
numbering(el.numbering, ..counter.at(el.location()))
}
let page-numbering = el.location().page-numbering()
if page-numbering == none { page-numbering = "1" }
let page = numbering(page-numbering, ..counter(page).at(el.location()))
(num, body + [ ] + box(width: 1fr, it.fill), page)
}).flatten()
)
}