How to right annotate a span of list items with a brace and text?

I want to annotate list items with a right brace and text, and only some of them.

I currently have a janky math based version which is unsatisfactory, and furthermore, changes the font.

Current effort:

Example code:

#set math.cases(reverse: true)

$
  #[
    - Item 1
    - Item 2
  ] \

  cases(
    #[
      - Item 3
      - Item 4
      - Item 5
    ]
  ) "two times" \

  #[
    - Item 6
  ]
$

Very hacky and limited, but it works:

#let get-margin(side) = {
  if page.margin == auto or page.margin.at(repr(side)) == auto {
    calc.min(page.height, page.width) * 2.5 / 21
  } else {
    page.margin.at(repr(side))
  }
}

#let from() = [#metadata(none)<from>] + hide[.]
#let to() = [#metadata(none)<to>] + hide[.]
#let brace(dx: 1em, text) = context {
  let from = query(<from>).last()
  let to = query(<to>).last()
  let (_, x1, y1) = from.location().position().values()
  let (_, _, y2) = to.location().position().values()
  let left-margin = get-margin(left)
  let top-margin = get-margin(top)
  let magic-y-offset = 0.7em
  place(
    top + left,
    dx: x1 - left-margin,
    dy: y1 - top-margin - magic-y-offset,
    $ stretch(\}, size: #(magic-y-offset + 100% * (y2 - y1))) $,
  )
  place(
    top + left,
    dx: x1 - left-margin + dx,
    dy: (y2 + y1) / 2 - top-margin - magic-y-offset,
    text,
  )
}

- Item 1
- Item 2
- Item 3 #from()
- Item 4
- Item 5 #to()
- Item 6

#brace[two times]

Works for item gap 0+.

Margin function adapted from How can I robustly place a box exactly on the left edge of each page? - #4 by vmartel08.

The main limitation for cleanest approach is this: "Real" absolute option for `place` (or alternative function) · Issue #5233 · typst/typst · GitHub.

1 Like

Hi, that’s a clever approach. Almost perfect, only problem is an intermediate list item having greater width than the end points would be overflowing into the brace. Maybe show rule for list and inserting labels at the end of each list item body and keeping track of max width of each of those as well?

Yeah, a different approach should be used to combat correct dx for multi-line list items… It’s probably possible with enough hacks (and probably a lot of time to craft them), but I don’t have time for this, sorry. Someone else might be up for the challenge.

When I first read this question I thought about using a grid. @Andrew’s solution is definitely still a list and will be styled accordingly. If you are willing to sacrifice some consistency, here is a grid-based solution:

#let annotated-list(from, to, note, list-body) = {
  show list: it => {
    let marker = it.marker.at(0)
    let rows = ()
    let row-num = 0
    
    //Iterate over items in list
    for child in it.children {
      row-num += 1
      let cells = ()
      cells.push(marker)
      cells.push(child.body)
      
      //Add cells based on whether there is a note or not
      if row-num == from {
        let span-cell = grid.cell.with(rowspan: to - from + 1, align: horizon)
        
        //The line - style as you want
        cells.push(span-cell(fill: gray, none))
        
        //The text to the right of the line
        cells.push(span-cell(note))
        
      } else if row-num < from or row-num > to {
        //Nothing special, just add empty spacing cells
        cells.push(none)
        cells.push(none)
      }
      //Add these cells to the row
      rows.push(cells)
    }
    grid(
      // stroke: .5pt,
      gutter: .25em,
      inset: .15em,
      columns: (auto, auto, .25em, auto),
      ..rows.flatten()
    )
  }
  
  //Display list which will be modified into grid
  list-body
}

#annotated-list(1, 3, "test")[
  - first
  - second
  - third
  - fourth
]
  - first
  - second
  - third
  - fourth

It iterates over the items of the list and adds them to an array as cells along with some special cells that contain the annotation. Then this array of cells is displayed in a grid. If you uncomment the // stroke: .5pt, you’ll better see how it is constructed.
I tried to stretch a brace but didn’t get anything to work so this will have to be a proof-of-concept for now.