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

Truncate statusline widgets #10087

Closed
wants to merge 1 commit into from

Conversation

hamrik
Copy link
Contributor

@hamrik hamrik commented Apr 1, 2024

Closes #10078.

Keeping PR as draft as the current solution while technically fixes #10078 it is incomplete nonetheless. For example, it would not fix the right side overflowing.

The current fix simply discards the last few elements from statusline.left until the remaining widgets can be fully rendered.
The center and right sections are still hidden, though. I'll try to make it show as many widgets as possible, then open the PR for merging.

The issue is that a smarter layout strategy is required, that can reconcile which widgets to render given the constraints.
The current mess of if-else statements have proven to be inadequate, and I apologize for that.

A long-term solution could take a leaf out of Flutter's book, where the status line would be broken up into sub-widgets and layout widgets. The render function could then reconcile the constraints against each child widget:

  1. Each widget receives the maximum allowed size it can occupy and returns the preferred size (based on children, text length, etc). Text widgets can use this info to truncate themselves, layout widgets can compute spacing, etc.
  2. Each widget then receives a Surface and target position to draw themselves/their children at.

This worked out pretty well for flutter and requires no backtracking (like we sorta need to with the current implementation). As a matter of fact, View already does something similar with gutters.

@archseer
Copy link
Member

We should fix this with some expediency if we want to release it this month.

I think this needs to be smarter and match the old behaviour more closely: a long filename shouldn't simply vanish off the statusline, it should be truncated to maximum possible width.

@hamrik
Copy link
Contributor Author

hamrik commented Apr 12, 2024

That could be done in one of two ways:

  • Tell each widget how much space there is available, and let the widget decide how to fit
    • E.g. file-path, version-control, and other long string elements might truncate themselves with ellipsis
    • Other elements might decide not to render at all.
    • This would be roughly what the proposed Flutter-like layout system does, but no discussion has yet been made on that
  • A quick-and-dirty solution could be to truncate the joined spans in render_statusline. This would avoid adding extra logic to every single element but might look awkward.

We also have to decide what to do in case multiple sides overflow.
For example, if we have 80 columns available, center takes 10 and both left and right are 90, what should we render? The current logic will try to render left. If we truncate it, that will work but no info from center or right will be available. Should we try to even out the truncation? Hide center to render 40 from left and 39 from right? Which end of right to truncate? Etc...

If left and center would fit but right will overflow, what then? Currently we hide both center and right entirely, again hiding info we shouldn't. Should we try to truncate right in that case?

@pascalkuthe
Copy link
Member

Yeah the old system didn't handle these cases particularly well either.

I like what you propose in general but I think providing a maximum width to elements won't work since you can't know ahead of time whether the size is exceeded (or what the maximum size of an element would be).

We would probably need to just need to render twice: One to figure out the width. Of zhere is enough width bothing happens. Then we first try to remove empty spacing, if that doesn't work I would go right to left and try to truncste elements to their preffered width (rendering them again with truncate=remaining_space_to_truncste) the filepath, would just insert an elipsis bit try to keep the filename intact (unless the filename is longer than day 10 charactes). If that still doesn't work then we would drop elements from right to left until there is enough space

@hamrik
Copy link
Contributor Author

hamrik commented Apr 12, 2024

Yeah, what you describe is roughly how both Flutter and the current gutter implementation does it.

A build step grabs the info from RenderContext and the statusline config and assemble a tree of Widgets initialized with the content that should render. The current statusline element functions (render_mode, etc) would become builder functions that return Widgets.

Here's a rough example of a resulting tree following the Flutter analogy:

Statusline {
  left: Row { children: [ 
    Span('NOR'), 
    Path('/a/b/c/d.rs'), 
    Span('[readonly]'), 
    Span('[+]'),
  ] },
  center: Row { children: [ ] },
  right: Row { children: [ 
    Spans([Span('W '), Span('•', yellow), Span(' 1')]), 
    Span('some-branch'),
  ] }
}

Each widget could indicate how much space they would take given a size constraint (size(max_width) -> usize). This function could be called multiple times with different inputs without actually rendering the widget. Some widgets could maybe indicate that they're flexible and would be shrunk first when space is low (e.g. Path). Then after some layout optimization the widgets would be actually rendered to a surface in a second pass (render(surface, x, y, max_width)).


Of course figuring out the details for this would take way longer than we have until the next release so for here's an idea:

  1. Indicate somehow which elements are "flexible" and which are all-or-nothing. We could maybe even assign a priority to each.
  2. Render all of the inflexible elements and sum up the space they take.
  3. If they take more space than availble start removing the largest / least prioritized ones until the remaining ones fit.
  4. If there is space left distribute it evenly among the flexible widgets and render them with the proper truncation etc.
  5. If there is still space left align the center section.

@the-mikedavis
Copy link
Member

I wonder if we should just lean on the existing Layout, Constraint and set_spans_truncated from helix-tui? Each component could return a Constraint that for most statically sized components would be their length.

@pascalkuthe
Copy link
Member

I wonder if we should just lean on the existing Layout, Constraint and set_spans_truncated from helix-tui? Each component could return a Constraint that for most statically sized components would be their length.

I keep forgetting that exists :D But yeah that is the same direction I was suggestion. Make the statusline components return somekind of constraint first, solve the size for each component and then make each component render with the max size given to them and figure out what to do when there isn't enough space

@hamrik
Copy link
Contributor Author

hamrik commented Apr 29, 2024

Turns out this is a very deep rabbit hole, that I unfortunately don't have time to explore these days.

I'll cook up a rough solution by tomorrow based on the outline below, and leave integrating cassowary to someone else.

  1. There will be a new helper type, StatuslineWidget with extra metadata and helper variables. It implements From<Spans<'a>> so most existing render functions are unaffected.
  2. The most important new field of this type is shrinkable, which is enum of None, Start and End, which determines which side of the string to truncate if there is not enough space. The default is None, in which case the widget is considered "fixed", and is either shown entirely or not at all.
  3. The render_statusline function builds all the widgets, then checks if all of the fixed widgets would fit without any of the shrinkable ones.
    • If not, then the largest fixed widget is removed until the rest fits.
  4. The remaining space (total space minus the space taken by the remaining fixed widgets) is distributed among the skrinkable widgets in the following way:
    • Available space is distributed evenly among the skrinkable widgets.
    • If a skrinkable widget is smaller than its allotted space, it becomes a fixed widget and step 4 starts over
    • If all remaining shrinkable widgets are larger than their allotted space or no more shrinkable widgets are left, step 4 is complete.
  5. The skrinkable widgets are truncated according to the shrinkable enum.
  6. The center section's margins are computed similarly to how they are now, but the logic for determining which sections to render is no longer needed.
  7. The complete Spans is assembled and returned.

I couldn't think of a simpler way that wouldn't unnecessarily hide information. If this is too much the current commit in the PR is a lot simpler and fixes the linked issue but hides a lot of widgets that could otherwise be shown.

@archseer
Copy link
Member

Since we're close to a release and I don't want to cut more fix releases afterwards I'm leaning towards reverting the implementation in master and waiting until the changes are fully fleshed out and tested. The original change was a refactor anyway so we're not losing any features by doing so.

@hamrik
Copy link
Contributor Author

hamrik commented Apr 30, 2024

Reverted in #10642, not tested yet.

@hamrik hamrik closed this Apr 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Whole statusline "empties out" when filename doesn't fit
4 participants