Skip to content

Add basic tab group rendering for horizontal tabs#12089

Merged
johnturcoo merged 5 commits into
masterfrom
johnturco/app-4641-horizontal-tab-grouping-basic-rendering
Jun 7, 2026
Merged

Add basic tab group rendering for horizontal tabs#12089
johnturcoo merged 5 commits into
masterfrom
johnturco/app-4641-horizontal-tab-grouping-basic-rendering

Conversation

@johnturcoo
Copy link
Copy Markdown
Contributor

Description

Adds basic rendering for horizontal tab groups in the top tab bar, on top of the existing vertical tab grouping data model. A group renders as a single tab-bar slot containing a header (icon collage + name) followed by its member tabs.

  • Click the header → collapse/expand the group (collapsed shows just the header).
  • Double-click the header → rename inline.
  • Right-click the header → open the group context menu (labels adapt to "left/right" for horizontal layouts, "up/down/above/below" for vertical).
  • Right-click a member tab → standard tab menu, including "Remove from group".

Dragging is out of scope for this PR — neither groups nor members can be dragged yet.

All new behavior is gated behind FeatureFlag::GroupedTabs; when the flag is off, the horizontal tab bar renders exactly as before.

Changes

workspace/view.rs

  • Added TabBarSlot { Single | Group } enum and a preprocessing pass in render_tab_bar_contents that turns the flat tab list into a list of slots (ungrouped tabs become Single, contiguous same-group runs collapse into Group).
  • Added render_horizontal_tab_group (container + member tabs) and render_horizontal_tab_group_header (icon collage + name + click/double-click/right-click handlers).
  • Added compute_group_member_kinds — picks up to 4 distinct pane icons for a group by reusing each member tab's "Summary pair" selection, so the group icons are always a subset of what the member tabs would show in vertical Summary.
  • Added render_group_member_icon_collage — 1 or 2 icons reuse vertical Summary's Single/Pair layout for visual parity; 3 or 4 fall back to a corner collage. Designs for reference:
Screenshot 2026-06-02 at 4 14 24 PM Screenshot 2026-06-02 at 4 14 52 PM
  • Added select_unique_pane_kinds — shared selection helper used by both vertical Summary (select_summary_pane_kind_icons) and the horizontal collage.
  • Added per-group hover state map (horizontal_tab_group_mouse_states) on Workspace.
  • tab_group_menu_items now takes is_vertical: bool so labels adapt to layout.

tab.rs

  • TabComponent gained a grouped_member flag and .for_grouped_member() builder. In that mode the tab skips side dividers, paints an inset rounded highlight, and skips the Draggable wrapper (member dragging is a follow-up).

workspace/view/vertical_tabs.rs

  • render_summary_pane_kind_icons and SummaryPaneKindIcons are now pub(super) and accept a total_size parameter so the horizontal collage can reuse them at its own sizing.
  • select_summary_pane_kind_icons now delegates to the shared select_unique_pane_kinds helper.

Linked Issue

https://linear.app/warpdotdev/issue/APP-4641/horizontal-tab-grouping-basic-rendering

Testing

  • I have manually tested my changes locally with ./script/run

Screenshots / Videos

Demo Video

@cla-bot cla-bot Bot added the cla-signed label Jun 2, 2026
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented Jun 2, 2026

@johnturcoo

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds the first horizontal tab-group rendering path, including grouped slots in the top tab bar, group headers, member-tab styling, and shared summary-icon selection with vertical tabs. The description includes visual evidence, and the spec context says no approved repository spec was available.

Concerns

  • Collapsed horizontal groups stop rendering member tabs without replacing their saved tab-position anchors, so drag insertion can keep using stale member positions and produce insertion indices inside hidden groups.
  • Horizontal group menu labels are derived from the vertical-tabs user preference rather than the actually rendered layout, so a horizontal header can show vertical wording when the vertical-tabs panel is closed.

Verdict

Found: 0 critical, 2 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/workspace/view.rs
ctx,
));

if !is_collapsed {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] When a group is collapsed, member tabs stop refreshing their tab_position_id SavePosition entries and the header does not write a replacement. tab_insertion_index_for_cursor still reads those cached tab positions, so after collapsing a previously expanded group, stale member rects can return insertion indices inside the hidden group while this renderer only draws a ghost before the group. Dropping there can split the group/run; add a saved group/header rect and map collapsed groups to the correct boundary, or otherwise replace/ignore hidden member positions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be resolved in my next PR that implements dragging support. Trying to drag into a group currently results in undefined behavior.

Comment thread app/src/workspace/view.rs
@johnturcoo
Copy link
Copy Markdown
Contributor Author

johnturcoo commented Jun 2, 2026

Note: Follow-up PR coming to move the bulk of the tab grouping logic out of workspace/view.rs and into a new file called tab_grouping.rs — this file is getting large and the grouping changes warrant their own module.

Comment thread app/src/workspace/view/vertical_tabs.rs
Comment thread app/src/workspace/view/vertical_tabs.rs Outdated
Comment thread app/src/workspace/view/vertical_tabs.rs Outdated
Comment thread app/src/workspace/view/vertical_tabs.rs
Comment thread app/src/workspace/view.rs Outdated
})
}

// Render each slot in the tab bar, either an indiovidual tab or tab group.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

Copy link
Copy Markdown
Contributor

@peicodes peicodes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two thoughts:

  • We should be very confident existing horizontal tab dragging still works, incl dragging out into a new window, please make sure that's being tested
  • The descriptions for PRs should be formatted to better suit a human reader who doesn't have knowledge of how tab dragging and grouping works. No need to enumerate code changes; it's much more helpful to know (in a few pragraphs) how it roughly works, what deviations you had to make with existing code, what things you did that might be sus and you want an extra pair of eyes on

Comment thread app/src/workspace/view.rs

/// A unit the horizontal tab bar renders: either a single ungrouped tab or a
/// contiguous run of same-group tabs collapsed into one group container.
enum TabBarSlot {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should leave a note on Vertical tabs (where that loop is) that we should use this instead

Comment thread app/src/workspace/view.rs Outdated
header.finish()
}

/// Up to 4 distinct pane icons for the group's collage. Each member tab
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think this description should focus on what this function does and why instead of how it's done. It would be helpful to illustrate with an example

Comment thread app/src/workspace/view.rs
{
if *last_gid == group_id {
*run_len += 1;
continue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: whenever you have looping logic with early returns, it helps to explain what case that's covering

Comment thread app/src/workspace/view.rs
}

// Render each slot in the tab bar, either an indiovidual tab or tab group.
for slot in &slots {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if we extract this to a function, it'll be more obvious to a reviewer what the blast radius of this code is

Comment thread app/src/workspace/view.rs
}
TabContextMenuAnchor::Pointer(position) => {
let positioning = match (is_vertical, anchor) {
(true, TabContextMenuAnchor::VerticalTabsKebab) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think a match on multiple vars is an anti-pattern because it makes it harder to tell at a glance if all cases are covered

Comment thread app/src/workspace/view.rs
.with_height(GROUP_ICON_COLLAGE_SIZE)
.finish(),
);
for (idx, kind) in kinds.iter().take(count).enumerate() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another example of where filter/map is probably possible

Comment thread app/src/workspace/view.rs
appearance,
);
let offset = match (count, idx) {
(3, 0) => vec2f(-diag, -diag),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// 3 items in group, 1st item

@johnturcoo johnturcoo enabled auto-merge (squash) June 7, 2026 02:18
@johnturcoo johnturcoo merged commit d375729 into master Jun 7, 2026
25 checks passed
@johnturcoo johnturcoo deleted the johnturco/app-4641-horizontal-tab-grouping-basic-rendering branch June 7, 2026 02:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants