diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8d6c4ea..397243d 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -28,6 +28,7 @@ full = [ "input", "popover", "scroll_area", + "tooltip", "menu", "dialog", "sheet", diff --git a/crates/core/src/button.rs b/crates/core/src/button.rs index 81efa88..c43e84d 100644 --- a/crates/core/src/button.rs +++ b/crates/core/src/button.rs @@ -2,17 +2,20 @@ use gpui::prelude::FluentBuilder as _; use gpui::{ - App, BoxShadow, ClickEvent, ElementId, InteractiveElement as _, IntoElement, + AnyView, App, BoxShadow, ClickEvent, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Window, px, relative, }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::{ActiveTheme as _, Size}; +use std::rc::Rc; use crate::common::{Sizable, h_flex}; /// The single activation handler, shared by pointer, Enter, and Space. type ClickHandler = Box; +/// The tooltip view builder gpui's `.tooltip(...)` expects. +type TooltipBuilder = Rc AnyView>; /// Which visual treatment a button uses. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -38,6 +41,7 @@ pub struct Button { size: Size, disabled: bool, a11y_label: Option, + tooltip: Option, on_click: Option, } @@ -55,6 +59,7 @@ impl Button { size: Size::default(), disabled: false, a11y_label: None, + tooltip: None, on_click: None, } } @@ -74,6 +79,15 @@ impl Button { self } + /// Shows a tooltip view next to the button after it is hovered. + pub fn tooltip(mut self, tooltip: F) -> Self + where + F: Fn(&mut Window, &mut App) -> AnyView + 'static, + { + self.tooltip = Some(Rc::new(tooltip)); + self + } + /// Sets the visual treatment. pub fn variant(mut self, variant: ButtonVariant) -> Self { self.variant = variant; @@ -173,6 +187,9 @@ impl RenderOnce for Button { }) .when_some(self.label, |button, label| button.child(label)) .when(self.disabled, |button| button.opacity(0.5)) + .when_some(self.tooltip, |button, tooltip| { + button.tooltip(move |window, cx| tooltip(window, cx)) + }) .when(!self.disabled, |button| { button .cursor_pointer() diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 7e4869d..531d620 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -45,6 +45,9 @@ pub mod popover; #[cfg(feature = "scroll_area")] pub mod scroll_area; +#[cfg(feature = "tooltip")] +pub mod tooltip; + /// Initializes everything the components need at startup. pub fn init(cx: &mut App) { gpui_native_theme::Theme::init(cx); diff --git a/docs/PLAN.md b/docs/PLAN.md index 38a9ac5..7b792dc 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -212,7 +212,7 @@ Each row is one review unit: build it, show it in the gallery, review, then move | 11.5 | core | ScrollArea + Scrollbar | ✅ Landed (2026-10-29). Inserted before the remaining overlays — Menus, Lists and Tables all scroll. gpui owns the mechanics (`overflow_*_scroll` + caller-held `ScrollHandle`); the component paints an overlay thumb (`scrollbar_thumb` token, `Scrollbar` metrics) with drag and geometry unit-tested. Wheel input is gpui's; the area stops wheel propagation so nested areas don't co-scroll. Horizontal thumb deferred; auto-hide on idle deferred | | 12 | core | Menu | ✅ Landed (2026-10-29). Controlled `open`/`on_change` + `trigger`/`items` (MenuItem: label, icon, shortcut hint, disabled, destructive, checked, separator). Popover shell patterns duplicated per D16 (promote at Dialog); panel = surface + overlay radius + popover elevation, role Menu; claims focus while open; keyboard nav — arrows/home/end over selectable items (wrapping, skipping separators + disabled), enter/space activates (closes menu before the action), escape dismisses; hover selects, click activates; trigger clicks toggle (popover-owned); selection resets on each open; long menus scroll via the ScrollArea once a height is styled. Deferred: submenus, type-ahead, scroll-selected-item-into-view, real shortcut handling | | 11 | core | Popover | ✅ Landed (2026-10-29). First overlay. Controlled `open` + `on_change` (Rc-shared handler); `trigger`/`content` element closures; panel = surface + `Radius.overlay` + `Elevations.popover` + border, role Dialog with label; `anchored()` + `deferred()` (window-overflow flip for free); enter fade via gpui `with_animation` at `Motion.overlay` (respects reduce-motion); claims keyboard focus while open; escape and click-outside dismiss (outside clicks swallowed). No scrim — native macOS popover model; the overlay token waits for Dialog. Known v1 gaps: no exit animation, trigger aria-expanded left to the app | -| 11+ | core | Tooltip, Menu, Dialog, AlertDialog, Sheet, Toast | One unit each, same gate. Menu reuses the popover shell + `Radius.overlay`/`Elevations.popover`; Dialog consumes `colors.overlay` (scrim) + `Elevations.dialog` | +| 11+ | core | Menu ✅ (2026-10-29), Tooltip ✅ (2026-10-29), Dialog, AlertDialog, Sheet, Toast | One unit each, same gate. Menu reuses the popover shell + `Radius.overlay`/`Elevations.popover`; Dialog consumes `colors.overlay` (scrim) + `Elevations.dialog`. Tooltip rides gpui's own `.tooltip()` lifecycle (hover delay, anchoring, dismissal — all framework-owned); ours is the styled view (`surface` + border + `Radius.sm`, small text) plus a `Tooltip::text` closure and a Button passthrough | | then | data (new) | List, Table, Tree, Tabs, Sidebar | Created on demand, same gate. | | then | pickers or data | Calendar/DatePicker, TimeField, ColorPicker | Decide at unit time. | | editor | editor (new) | Text/code editor, syntax highlighting (D12) | Same gate. | diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 9023644..f5519be 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -18,6 +18,7 @@ use gpui_native_core::scroll_area::ScrollArea; use gpui_native_core::slider::Slider; use gpui_native_core::switch::Switch; use gpui_native_core::toggle::Toggle; +use gpui_native_core::tooltip::Tooltip; use gpui_native_icons::{IconAssets, IconRole, icon}; use gpui_native_theme::{ActiveTheme as _, Theme}; use gpui_platform::application; @@ -190,15 +191,22 @@ impl Render for Gallery { .gap_2() .children(button_variants.map(|(name, variant)| { let gallery = gallery.clone(); - Button::new(SharedString::from(format!("gallery-button-{name}"))) - .label(button_label(variant)) - .variant(variant) - .on_click(move |_, _, cx| { - gallery.update(cx, |gallery, cx| { - gallery.button_clicks += 1; - cx.notify(); - }); - }) + let mut button = Button::new(SharedString::from(format!( + "gallery-button-{name}" + ))) + .label(button_label(variant)) + .variant(variant) + .on_click(move |_, _, cx| { + gallery.update(cx, |gallery, cx| { + gallery.button_clicks += 1; + cx.notify(); + }); + }); + if variant == ButtonVariant::Destructive { + button = + button.tooltip(Tooltip::text("Removes the selected item")); + } + button })) .child( Button::new("gallery-button-disabled") @@ -208,7 +216,8 @@ impl Render for Gallery { .child( Button::new("gallery-button-icon") .label("Open") - .icon(IconRole::Folder), + .icon(IconRole::Folder) + .tooltip(Tooltip::text("Opens the selected file")), ), ) .child(