Skip to content

Commit 699936e

Browse files
authored
button: expose preferred tooltip placement (#2971)
Button tooltips always use automatic positioning, even though the existing tooltip overlay supports a preferred side. This adds `tooltip_placement(Placement)` so callers can choose that side without replacing the button’s tooltip handling. ```rust Button::new("search") .label("Search") .tooltip("Find a document") .tooltip_placement(Placement::Left) ``` It works with both `tooltip(...)` and `tooltip_with_action(...)`. The side is a preference, not a fixed position. The existing positioner still flips and clamps the tooltip when space is limited. Omitting the setting preserves automatic positioning. Setting it without tooltip content does nothing. ## Screenshots | Left | Right | | --- | --- | | ![Search tooltip on the left](https://github.com/user-attachments/assets/9ead6676-b4fd-4084-83cf-17231a563c3a) | ![Tooltip on the right of Hover me](https://github.com/user-attachments/assets/a44cbf23-f504-43ff-b100-1d369017b988) | | **Bottom, with shortcut** | **Top requested, falls back below** | | ![Info tooltip below the button with its shortcut](https://github.com/user-attachments/assets/993918b3-d9f9-4e2e-8d42-d29ac06b4d94) | ![At the top edge, the tooltip falls back below the button](https://github.com/user-attachments/assets/1f5ab695-cb31-43b8-8c56-0266563c5596) | ## Video Recorded during my manual testing: https://github.com/user-attachments/assets/094f000b-847b-4504-a31d-3fed9ecdd35d ## How to Test On macOS: ```sh MTL_HUD_ENABLED=1 ./script/run-story-macos Tooltip ``` Hover **Search**, **Info**, and **Hover me**, moving away between buttons. Their tooltips should appear on the left, below with the shortcut, and on the right. To check fallback near the window edge: ```sh cargo run -p tooltip_top_edge --locked ``` Hover the top button. It requests `Top`, but the tooltip should appear below because there is no room above. <details> <summary>Automated checks</summary> ```sh cargo test -p gpui-base -p gpui-component --lib --locked cargo clippy -p gpui-base -p gpui-component -p gpui-component-story --all-targets --locked -- -D warnings ``` 771 base tests and 418 component tests passed. Strict Clippy and formatting passed. The top-edge example also passed its build and strict Clippy check. There is no new automated test for the Button forwarding method. Placement was checked visually on macOS. </details> ## Checklist - [x] Read [CONTRIBUTING.md](https://github.com/longbridge/gpui-kit/blob/main/CONTRIBUTING.md) and followed the guidelines. - [x] Reviewed the changes, including the AI-assisted code. - [x] Ran and manually tested the related Story examples on macOS. This change is not platform-specific. Windows and Linux runtime and performance were not tested. ## AI assistance I tested the change in my application, then asked OpenAI’s `gpt-6-astra` to extract it into this PR. The agent prepared the patch, examples, verification scripts, and description, and ran the automated checks. I reviewed the extracted patch, requested corrections, and tested it myself on macOS.
1 parent 21e802b commit 699936e

4 files changed

Lines changed: 44 additions & 17 deletions

File tree

crates/component/src/button/button.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use std::rc::Rc;
22

33
use crate::ThemeStyled as _;
44
use crate::{
5-
ActiveTheme, Colorize as _, Disableable, Icon, RoleOverride, Selectable, Sizable, Size,
6-
StyleSized, StyledExt,
5+
ActiveTheme, Colorize as _, Disableable, Icon, Placement, RoleOverride, Selectable, Sizable,
6+
Size, StyleSized, StyledExt,
77
button::ButtonIcon,
88
h_flex,
99
select::Caret,
@@ -209,6 +209,7 @@ pub struct Button {
209209
SharedString,
210210
Option<(Rc<Box<dyn gpui::Action>>, Option<SharedString>)>,
211211
)>,
212+
tooltip_placement: Option<Placement>,
212213
tooltip_builder: Option<Rc<dyn Fn(&mut Window, &mut App) -> gpui::AnyView>>,
213214
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
214215
on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
@@ -252,6 +253,7 @@ impl Button {
252253
border_edges: Edges::all(true),
253254
size: Size::Medium,
254255
tooltip: None,
256+
tooltip_placement: None,
255257
tooltip_builder: None,
256258
on_click: None,
257259
focus_ring_enabled: true,
@@ -368,6 +370,15 @@ impl Button {
368370
self
369371
}
370372

373+
/// Prefer a side for the tooltip, falling back when it does not fit.
374+
///
375+
/// Applies to [`Self::tooltip`] and [`Self::tooltip_with_action`].
376+
/// Omitting placement keeps automatic positioning.
377+
pub fn tooltip_placement(mut self, placement: Placement) -> Self {
378+
self.tooltip_placement = Some(placement);
379+
self
380+
}
381+
371382
/// Set the tooltip of the button with action to show keybinding.
372383
pub fn tooltip_with_action(
373384
mut self,
@@ -536,6 +547,7 @@ impl RenderOnce for Button {
536547
let hoverable = self.hoverable();
537548
let disabled = self.disabled;
538549
let loading = self.loading;
550+
let tooltip_placement = self.tooltip_placement;
539551
let hover_group = self.hover_group;
540552
let hover_group_held = self.hover_group_held;
541553
let mut base = self.base;
@@ -780,9 +792,11 @@ impl RenderOnce for Button {
780792
})
781793
.map(|this| {
782794
if let Some(builder) = self.tooltip_builder {
783-
this.managed_tooltip(move |window, cx| builder(window, cx))
795+
this.managed_tooltip_with_placement(tooltip_placement, move |window, cx| {
796+
builder(window, cx)
797+
})
784798
} else if let Some((tooltip, action)) = self.tooltip {
785-
this.managed_tooltip(move |window, cx| {
799+
this.managed_tooltip_with_placement(tooltip_placement, move |window, cx| {
786800
Tooltip::new(tooltip.clone())
787801
.when_some(action.clone(), |this, (action, context)| {
788802
this.action(

crates/story/src/stories/tooltip_story.rs

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use gpui_kit::prelude::FluentBuilder as _;
22
use gpui_kit::*;
33

44
use gpui_kit::component::{
5-
IconName,
5+
IconName, Placement,
66
button::{Button, ButtonVariant, ButtonVariants, Toggle},
77
checkbox::Checkbox,
88
clipboard::Clipboard,
@@ -46,7 +46,7 @@ impl Story for TooltipStory {
4646
}
4747

4848
fn description() -> &'static str {
49-
"Describe a control on hover or keyboard focus."
49+
"Describe a control on hover."
5050
}
5151

5252
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render> {
@@ -75,22 +75,29 @@ impl Render for TooltipStory {
7575
.gap_3()
7676
.child(
7777
section("Button")
78-
.description("Add plain text or a keyboard shortcut hint.")
78+
.description("Prefer the left, bottom, or right side, with an optional keyboard shortcut hint.")
7979
.child(
8080
Button::new("btn0")
8181
.label("Search")
8282
.with_variant(ButtonVariant::Primary)
83-
.tooltip("This is a search Button."),
83+
.tooltip("This is a search Button.")
84+
.tooltip_placement(Placement::Left),
8485
)
85-
.child(Button::new("btn1").label("Info").tooltip_with_action(
86-
"This is a tooltip with Action for display keybinding.",
87-
&Info,
88-
Some("Tooltip"),
89-
))
9086
.child(
91-
Button::new("btn3")
87+
Button::new("btn1")
88+
.label("Info")
89+
.tooltip_with_action(
90+
"This is a tooltip with Action for display keybinding.",
91+
&Info,
92+
Some("Tooltip"),
93+
)
94+
.tooltip_placement(Placement::Bottom),
95+
)
96+
.child(
97+
Button::new("btn2")
9298
.label("Hover me")
93-
.tooltip("This is tooltip 3"),
99+
.tooltip("This tooltip prefers the right side.")
100+
.tooltip_placement(Placement::Right),
94101
),
95102
)
96103
.child(

examples/tooltip_top_edge/src/main.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use gpui_kit::component::{ActiveTheme as _, Root, button::*};
1+
use gpui_kit::component::{ActiveTheme as _, Placement, Root, button::*};
22
use gpui_kit::*;
33

44
struct TooltipTopEdgeExample;
@@ -15,7 +15,8 @@ impl Render for TooltipTopEdgeExample {
1515
Button::new("top-edge-tooltip")
1616
.primary()
1717
.label("Hover for tooltip")
18-
.tooltip("This tooltip should appear below the trigger near the top edge."),
18+
.tooltip("This tooltip should appear below the trigger near the top edge.")
19+
.tooltip_placement(Placement::Top),
1920
),
2021
)
2122
.child(

website/docs/components/button.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,13 @@ Button::new("custom-btn")
307307
Button::new("btn")
308308
.label("Hover me")
309309
.tooltip("This is a helpful tooltip")
310+
.tooltip_placement(Placement::Bottom)
310311
```
311312

313+
Use `.tooltip_placement(...)` to prefer a side for either `.tooltip(...)` or
314+
`.tooltip_with_action(...)`. The tooltip still flips when that side does not fit.
315+
Omit placement to keep automatic positioning.
316+
312317
### Custom Children
313318

314319
```rust

0 commit comments

Comments
 (0)