Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Balancing the contents of multiple columns to the same vertical space #466

Open
ghost opened this issue Mar 30, 2023 · 25 comments · May be fixed by #5115
Open

Balancing the contents of multiple columns to the same vertical space #466

ghost opened this issue Mar 30, 2023 · 25 comments · May be fixed by #5115
Labels
feature request New feature or request layout Related to layout, positioning, etc.

Comments

@ghost
Copy link

ghost commented Mar 30, 2023

I remember that in LaTeX the output of multicolumns will be balanced to the same vertical space in all columns to have enough space below that could start a new single column layout or an image or so.

In Typst the output for

#set par(justify: true)

#set page(columns: 2)

#lorem(500)

is currently unbalanced which looks not so professional, I think.

balanced

Maybe a parameter like balance-columns or columns: (balance: bool) can be added to make this possible.

@laurmaedje laurmaedje added feature request New feature or request layout Related to layout, positioning, etc. labels Mar 31, 2023
@myrrlyn
Copy link

myrrlyn commented Apr 3, 2023

I am interested in giving this a shot. I think I have a decent first draft of the requisite behavior. I'm going to describe it here to see if I'm on the right track:

My understanding (which I hope is correct!) is that a finitely-sized Region is a single PDF page, and that each Frame printed into a Region represents one rendered column of content. Assuming this is true, then my path forward is:

  1. guard the first body.layout() call with vt.provider.save()/vt.provider.restore(). This gives me a collection of Frames which I will then measure, and undoes any state changes that might have occurred during layout.
  2. Discard all the Frames in that collection which are not in the final Region. This is just frames.into_iter().skip(total_regions.saturating_sub(1) * columns).
  3. Find the average rendered height of the remaining Frames. This is now the goal height of the final Region.
  4. Reconstruct a laying-out backlog, using the full regions.size.y height for the Frames in all but the final Region, and then switching over to the goal height for all subsequent Frames.
  5. Layout body into that backlog to produce a new collection of Frames. This is the part that worries me: this layouting will need to be willing to overshoot the goal height, because it cannot be allowed to undershoot so much that additional Frames are produced.

I think this should result in a collection of laid-out Frames that are (almost) correctly balanced, and then the Region assembly loop can be left untouched.

However, my attempt at writing this out does not result in column-balancing behavior in the test suite. I've only just started reading the codebase, so I'm not at all informed as to where I should be looking to understand why the expected effects aren't occurring. I'll continue to investigate this on my own and see if I can figure out where my assumptions are failing to meet reality.

@reknih reknih mentioned this issue May 9, 2023
@xosxos
Copy link

xosxos commented Jun 13, 2023

Bump.. it would be very nice to have single column images and two column text mixed together

@astrale-sharp
Copy link
Contributor

Or a tight: true option

@laurmaedje
Copy link
Member

I would call it balanced. #set columns(balanced: true).

@eduardz1
Copy link

any developments?

1 similar comment
@KDr2
Copy link

KDr2 commented Jan 9, 2024

any developments?

@laurmaedje
Copy link
Member

Not yet

@Caellian
Copy link

Caellian commented Feb 2, 2024

In the meantime, a solution that works (only for fixed size content):

#let balanced-cols(content) = style(styles => {
  let h = measure(content, styles).height / 2
  block(height: h, columns(2, content))
})
  • It will add padding if content is odd number of fixed elements or the elements aren't even
  • It will break if the wrapped content spans multiple pages
  • EDIT: It only works with content that has an explicit height set (i.e. not text, boxed content, ...)

@astrale-sharp
Copy link
Contributor

#balanced-cols(lorem(200))
image

Color me unimpressed 🤣

more srsly, Am I using it wrong?

@Caellian
Copy link

Caellian commented Feb 3, 2024

Yeah, inline content doesn't have a fixed height. AFAICT measured height is min required height for content so you end up with h == 0 because a paragraph doesn't have a min height.

This is what happens when I add newlines:
image

I wrapped it in blocks because using boxes (inline element) breaks in a similar way. balanced-cols only works well when you have uniform children blocks that fit on a single page. I can't see any viable solution for text though.

@dahooz
Copy link

dahooz commented Apr 3, 2024

A possible workaround could be:

#let eqcolumns(n, gutter: 4%, content) = {
  layout(size => [
    #let (height,) = measure(
      block(
        width: (1/n) * size.width * (1 - float(gutter)*n), 
        content
      )
    )
    #block(
      height: height / n,
      columns(n, gutter: gutter, content)
    )
  ])
}

This uses #layout to get the surrounding width (size.width), calculates the column width depending on gutter and column number n and typesets the content under that width in a #block. Then it takes the #measured height of this block, and typesets the actual #columns in a #block of the calculated height divided by the column number n, so that the #columns has a forced height, which forces it to reflow the content accordingly.

This works for one-paragraph text and reasonably well for multi-paragraph-text (it looks ugly if a paragraph break is close to a column break, because it does not treat the column break as replacing the paragraph break). It will also break if the total content height is longer than one page!

@soleilvermeil
Copy link

1
For the simple reason that one of typst's mottos is "Compose papers faster", and noticing that most articles are written in two columns, I find it surprising that this feature isn't already implemented.

@dahooz's workaround is interesting, but does not work great for papers of over a page, which is unfortunate.

@Samt43
Copy link
Sponsor

Samt43 commented Aug 30, 2024

I was also looking for this feature, quite important from my point of view. Can't wait to see it on the roadmap ! Thanks a lot for all the work already done :)

@Caellian
Copy link

Caellian commented Sep 9, 2024

Looking at the FlowLayouter impls, there seems to be enough information there to implement eqcolumns in a way that works across pages and with edge cases better. I'm not a long time LaTeX user so I'm not sure about expected behavior in all the edge cases but we'll see.

I'll give a go to implementing this in a week or so, my progress is here in case I quit.
I have to compliment the devs, the codebase is absolutely beautiful at the first glance (can't wait to ruin it 😆).

@xosxos
Copy link

xosxos commented Sep 11, 2024

@Caellian If you haven't noticed, a refactor of flow.rs was just merged yesterday #4931 and I couldn't rebase your code. Just a heads up in case your current ideas for the draft would be going against the grain.

@laurmaedje
Copy link
Member

Yes, the flow layout is indeed changing and will change a lot more soon. It might be best to wait until after that.

@Caellian
Copy link

Yes, the flow layout is indeed changing and will change a lot more soon. It might be best to wait until after that.

I'll hold off until then. My current implementation separates balancing into a wrapper struct that calls flow layout. Should I integrate it directly into flow layout instead if used private functions get spread across multiple modules?

Ideally, I'd like to be able to measure vertical height of each element given the width constraint of the Regions they're located in, and it's a bit tricky because that requires them to have a Location which they don't until flow layout runs, and I can foresee cases where that Location would become invalidated by balancing. If possible it would be very useful for that part of flow layout to be able to run separately for this purpose and possibly some other in the future.

@laurmaedje
Copy link
Member

Should I integrate it directly into flow layout instead if used private functions get spread across multiple modules?

Let's continue that discussion once the changes have landed! I think it'll be much more actionable then.

@laurmaedje
Copy link
Member

@Caellian The flow rewrite is merged now (#5017), so feel free to take a look at that.

The main feature is that layout of a single region can be restarted now, which I think could be quite useful for balancing columns.

The new preprocessed children representation is also much easier to deal with and measure because it pre-assigns Locations.

@Caellian
Copy link

@Caellian The flow rewrite is merged now (#5017), so feel free to take a look at that.

Thanks for letting me know.

The main feature is that layout of a single region can be restarted now, which I think could be quite useful for balancing columns.

I hope I won't need to do that unless I encounter some weird edge cases. Maybe for pages with columns. But the #columns element should in theory be able to layout children evenly in a single pass given that regions stay the same across pages and element height depends on region width which makes them both constant.

The new preprocessed children representation is also much easier to deal with and measure because it pre-assigns Locations.

This will be very helpful and removes the need for a pre-pass.

@laurmaedje
Copy link
Member

I hope I won't need to do that unless I encounter some weird edge cases. Maybe for pages with columns. But the #columns element should in theory be able to layout children evenly in a single pass given that regions stay the same across pages and element height depends on region width which makes them both constant.

Could you elaborate on your planned approach?

@Caellian
Copy link

Caellian commented Sep 27, 2024

I hope I won't need to do that unless I encounter some weird edge cases. Maybe for pages with columns. But the #columns element should in theory be able to layout children evenly in a single pass given that regions stay the same across pages and element height depends on region width which makes them both constant.

Could you elaborate on your planned approach?

I was planning on measuring children here, and then limiting how they get distributed by externally modifying Work.children slice here via a struct that has special drop handling. But I just figured out that styles can be modified from inside columns which would make those measurements wrong - so I'll need to layout those children at least twice.

Note that this is very WIP code. I'm not yet fully familiar with the codebase so this is far from final design. I'm just trying to get something to kinda-work atm.

@Wulfheart
Copy link

Seems quite complicated. I would expect something like columns(balance: true).

@laurmaedje
Copy link
Member

Seems quite complicated. I would expect something like columns(balance: true).

The discussion is about how it's implemented, not how it looks like for a user.

I was planning on measuring children here, and then limiting how they get distributed by externally modifying Work.children slice here via a struct that has special drop handling. But I just figured out that styles can be modified from inside columns which would make those measurements wrong - so I'll need to layout those children at least twice.

The nice thing about relayouting a region is that the measurements you get from layouting within the region are much more accurate and true to nature than if you'd try to measure upfront, especially with blocks breaking over pages.

@Wulfheart
Copy link

The discussion is about how it's implemented, not how it looks like for a user.

Thank you for your hard work! Didn’t have the full context.

@Caellian Caellian linked a pull request Oct 4, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request New feature or request layout Related to layout, positioning, etc.
Projects
None yet
Development

Successfully merging a pull request may close this issue.