How to create colored pills that span multiple lines? (multiline boxes)

I want to create bi-colored pills like those used by e.g. gitlab scoped labels.

This can be done in the following way (heavily based on tidy implementation) from @Mc-Zen : Typst

However, as shown in the document, such pills do not like to be very long, and they mess up line breaks if they are.

Basically, I need a way to split a horizontal box over multiple lines.
I tried to find a simple solution that would not require complex context calculations, but I failed.
Any idea?

Maybe you could achieve what you want with highlight. A nice solution would be

#let pill(a, b, fill: red) = {
  highlight(
    fill: fill, 
    stroke: fill, 
    radius: (left: 100pt),
    text(white, [~#a~])
  )
  highlight(
    fill: white, 
    stroke: fill, 
    radius: (right: 100%),
    text(fill, [~#b~])
  )
}

Unfortunately, after a linebreak, the rounded corners are repeated
image

At least it’s already much better than using boxes. You could trick around a bit by adding the rounded beginning (and also end but here I did only the beginning) separately:

#let pill(a, b, fill: red) = {
  highlight(
    fill: fill, 
    stroke: fill, 
    radius: (left: 100pt),
    [~~~~~]
  )
  h(-.7em)
  highlight(
    fill: fill, 
    stroke: fill, 
    text(white, [#a~])
  )
  highlight(
    fill: white, 
    stroke: fill, 
    radius: (right: 100%),
    text(fill, [~#b~])
  )
}

image
This is something you might be able to tune until it looks good.

4 Likes

Thanks for that!
However it still breaks if it’s the rightmost part to go to a new line:
image

But I’ll surely find some more time to play around it and see if I can find a possible solution based on highlight instead of box

I know, I was just giving the idea based on the left part but you can do the same with the right one, I think.

Note that highlight.stroke accepts a dictionary which you can use like this

#let pill(a, b, fill: red) = {
  highlight(
    fill: fill, 
    stroke: fill, 
    radius: (left: 100%),
    [~~~~~]
  )
  h(-.7em)
  highlight(
    fill: fill, 
    stroke: fill, 
    text(white, [#a~])
  )
  highlight(
    fill: none, 
    stroke: (y: fill), 
    text(fill, [~#b])
  )
  h(-.7em)
  highlight(
    fill: none, 
    stroke: (y: fill, right: fill), 
    radius: (right: 100%),
    text(fill, [~~~~~])
  )
}

image

Still not perfect maybe but betterer.

4 Likes

Getting there!

#let pill(a, b, fill: gray, radius: 100%) = {
  highlight(
    fill: fill, 
    stroke: fill, 
    radius: (left: radius),
    [~~~~~#sym.zws]
  )
  h(-.7em)
  highlight(
    fill: fill, 
    stroke: fill, 
    text(white, hyphenate: true, [#a~#sym.zws])
  )
  highlight(
    fill: none, 
    stroke: (y: fill), 
    text(hyphenate: true)[#sym.zws~#b]
  )
  h(-.7em)
  highlight(
    fill: none, 
    stroke: (y: fill, right: fill), 
    radius: (right: radius),
    text(fill, [~~~~~])
  )
}

#box(width: 9em)[
  #pill("definitely good even when")[it goes to new line!]
]

#box(width: 9em)[
  #pill("first")[line!]\
  #pill("second")[line! All good!]
  
  #pill("thirddddddddddddd")[line! All good!]
  #pill("fourth")[line! Less good!]
]

I had to also add a few sym.zws to force it to go to new lines and not mess it up trying to connect different highlights.

It produces:

As you can see the last pill is not great, but it is almost there. Any brilliant idea on that one?

For the last one you could add a word joiner sym.wj between the h and the last highlight to force the last word to stick to the rounded ending.

3 Likes

Indeed that works well!

#let pill(a, b, fill: gray, radius: 100%) = {
  highlight(
    fill: fill, 
    stroke: fill, 
    radius: (left: radius),
    [#sym.zws~~~~~#sym.wj]
  )
  h(-.7em)
  highlight(
    fill: fill, 
    stroke: fill, 
    text(white, hyphenate: true, [#sym.wj#a~#sym.zws])
  )
  highlight(
    fill: none, 
    stroke: (y: fill), 
    text(hyphenate: true)[#sym.zws~#b#sym.wj]
  )
  h(-.7em)
  highlight(
    fill: none, 
    stroke: (y: fill, right: fill), 
    radius: (right: radius),
    text(fill, [#sym.wj~~~~~#sym.zws])
  )
}

#box(width: 9em)[
  #pill("definitely good even when")[it goes to new line!]
]

#box(width: 9em)[
  #pill("first")[line!]\
  #pill("second")[line! All good!]
  
  #pill("thirddddddddddddd")[line! All good!]
  #pill("fourth")[line! Also good!]
  #pill("fifthhhhhhhhhhhhhhh")[line! Indeed also good!]
]

Results in:

To me this is almost perfect!

3 Likes

Good one! The word-joiner was what I was looking for but I didn’t find it because I had forgotten the name.

This sounds related: Add documentation about ways to customize breaking of words · Issue #5624 · typst/typst · GitHub.

1 Like

I’m still playing around with this to improve the final look and make it more generic. I’ll post here when I have a final version.

But, a question to the wise… this seems hackish at best.
Is there no other way to do this?
I mean, if someone were to write a package to do this, which route would one take?

I don’t understand what is missing in How to create colored pills that span multiple lines? (multiline boxes) - #8 by adriano. Is it too complex? You can open a new Questions topic for a different question.

This also sounds something like Allow adding content / configuring breakpoints of broken blocks · Issue #735 · typst/typst · GitHub, but horizontally.

It is… hackish.
In fact if you start applying it it shows several limitations:

  • different radiuses, fonts and font sizes require manual tuning of the spacing to look decent
  • there are weird artifacts. Look at what happens if I use modified text for the left side of a pill:

image

Even modifying the highlight’s edges did not seem to fix this.

#pill[text текст テキスト][next]

#highlight(stroke: 1pt)[text текст テキスト]

image

Looks like

1 Like

indeed, that’s probably what it is! Watching the issue, thanks!

1 Like