Skip to content

Support dragging actions for horizontal tab groups#12110

Open
johnturcoo wants to merge 2 commits into
johnturco/app-4641-horizontal-tab-grouping-basic-renderingfrom
johnturco/app-4649-horizontal-tab-group-dragging
Open

Support dragging actions for horizontal tab groups#12110
johnturcoo wants to merge 2 commits into
johnturco/app-4641-horizontal-tab-grouping-basic-renderingfrom
johnturco/app-4649-horizontal-tab-group-dragging

Conversation

@johnturcoo
Copy link
Copy Markdown
Contributor

Description

Adds dragging support for horizontal tab groups, building on the basic rendering from johnturco/app-4641-horizontal-tab-grouping-basic-rendering. Horizontal tab bars now support:

  • Dragging a whole group container left/right to reorder the group as a block.
  • Dragging an individual group member out of the group, or a non-grouped tab into a group.
  • Dragging the lone member of a single-tab group, which drags the entire group rather than orphaning the tab.
  • Stable behavior when dragging across a collapsed group

The implementation re-uses the existing vertical-tab group-drag plumbing (StartGroupDrag / DragGroup / DropGroup, move_group_block, assign_tab_to_group, hop_tab_to_index) and generalizes the axis-specific helpers (neighbor_drag_rect, target_group_at_axis, on_group_drag) so both layouts share one code path.

Changes

workspace/view.rs

Wired up a group-level Draggable in render_horizontal_tab_group that dispatches the existing StartGroupDrag / DragGroup / DropGroup actions, with DragAxis::HorizontalOnly and child-mouse-down deferral so member tabs still drag individually. Generalized the axis-specific helpers used by both layouts:

  • target_group_at_axis — replaces the prior target_group_at_y / target_group_at_x pair; one is_vertical-parameterized hit-test.
  • neighbor_drag_rect — now takes is_vertical and falls back to the visible group container rect when the neighbor is a hidden collapsed-group member.
  • on_group_drag — generalized over both axes; uses neighbor-midpoint thresholds for vertical and neighbor-edge thresholds for horizontal so each layout matches its per-tab drag feel.

tab.rs

TabComponent gains grouped_member / sole_grouped_member flags, set via a new for_grouped_member(is_sole_member) builder. Grouped members get the regular per-tab Draggable; the sole-member case suppresses it so the parent group's Draggable picks up the drag.

workspace/view/vertical_tabs.rs

Added htab_group_position_id, the horizontal counterpart to the existing vtab_group_position_id, used by the new hit-test and drag-rect helpers.

Linked Issue

https://linear.app/warpdotdev/issue/APP-4649/horizontal-tab-group-dragging

Testing

  • I have manually tested my changes locally with ./script/run
  • Tested cross window drags, currently dragging from another window into a group is not supported

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

Reviewed the horizontal tab group dragging changes across TabComponent, horizontal group rendering, shared drag hit-testing helpers, and the new horizontal group save-position id. The PR includes visual evidence, and the attached spec context states that no approved or repository spec context was found.

Concerns

  • No blocking correctness, security, or material spec-alignment concerns were found in the annotated diff.

Verdict

Found: 0 critical, 0 important, 0 suggestions

Approve

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

Powered by Oz

@peicodes
Copy link
Copy Markdown
Contributor

peicodes commented Jun 3, 2026

Visual feedback: I think the tab group looks a little small - it has a different size than standard tabs, I feel like they should match?

/// Save-position id for a horizontal tab group's container rect, used for
/// drop hit-testing and as the collapsed-group fallback in horizontal-axis
/// drag math.
pub(crate) fn htab_group_position_id(group_id: TabGroupId) -> String {
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.

does something like this exist for vtabs? Or is it taking a different approach?

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.

Yes, the equivalent for vtabs is found directly above this (vtab_group_position_id). The reason for its existence is to handle two edge cases when dragging tabs into/out of groups:

  1. The group is expanded, can we drag into the first or last position of the group without dragging past the current first/last tab in that group (hit testing for group bounds)
  2. The group is collapsed, we must drag the tab past the entire group and not compare with a neighboring tab (which would be inside the group)

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

// Skip the group `Draggable` while another drag (a member tab via the
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.

How does this happen? I would expect there to be some kind of mutex on drag or that the drag is initiated by clicking and holding something (so it wouldn't be possible to initiate twice without release)

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.

The DraggableState mutex is enough here. This is redundant. Removing it

Comment thread app/src/workspace/view.rs Outdated
tab_bar_state.is_any_tab_dragging && !is_this_group_dragging;
let group_id = group.id;
let group_draggable_state = group.draggable_state.clone();
let positioned_container: Box<dyn Element> = if skip_group_draggable {
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: A guard clause makes this more readable. E.g.

if tab_bar_state.is_any_tab_dragging && !is_this_group_dragging {
  return container;
}
...

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.

Just questions to help my understanding - happy to stamp once I do

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