Add Tooltip (#5)

This commit is contained in:
Floyd Wang 2024-06-28 18:03:28 +08:00 committed by GitHub
parent 11901dc1e4
commit a5aea36f75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 324 additions and 8 deletions

View file

@ -38,6 +38,8 @@ This is an example of build app by using GPUI.
- [x] Tabs
- [x] Tab
- [x] TabBar
- [x] Notification
- [x] Tooltip
- [ ] Dockpanel & Splitter
- [ ] List
- [ ] Table

View file

@ -1,5 +1,5 @@
use gpui::{
div, px, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, ViewContext,
px, ClickEvent, IntoElement, ParentElement as _, Render, Styled as _, ViewContext,
WindowContext,
};

View file

@ -4,6 +4,7 @@ mod dropdown_story;
mod input_story;
mod picker_story;
mod switch_story;
mod tooltip_stroy;
use gpui::{
div, prelude::FluentBuilder as _, px, AnyElement, IntoElement, ParentElement, Render,
@ -25,6 +26,7 @@ use dropdown_story::DropdownStory;
use input_story::InputStory;
use picker_story::PickerStory;
use switch_story::SwitchStory;
use tooltip_stroy::TooltipStory;
pub fn story_case(name: &'static str, description: &'static str) -> StoryContainer {
StoryContainer::new(name, description)
@ -80,6 +82,7 @@ enum StoryType {
Switch,
Picker,
Dropdown,
Tooltip,
}
impl Display for StoryType {
@ -91,6 +94,7 @@ impl Display for StoryType {
Self::Switch => write!(f, "Switch"),
Self::Picker => write!(f, "Picker"),
Self::Dropdown => write!(f, "Dropdown"),
Self::Tooltip => write!(f, "Tooltip"),
}
}
}
@ -104,18 +108,20 @@ pub struct Stories {
switch_story: View<SwitchStory>,
picker_story: View<PickerStory>,
dropdown_story: View<DropdownStory>,
tooltip_story: View<TooltipStory>,
}
impl Stories {
fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
active: StoryType::Button,
active: StoryType::Tooltip,
button_story: cx.new_view(|_| ButtonStory {}),
checkbox_story: cx.new_view(|cx| CheckboxStory::new(cx)),
input_story: cx.new_view(|cx| InputStory::new(cx)),
switch_story: cx.new_view(|cx| SwitchStory::new(cx)),
picker_story: cx.new_view(|cx| PickerStory::new(cx)),
dropdown_story: cx.new_view(|cx| DropdownStory::new(cx)),
tooltip_story: cx.new_view(|_| TooltipStory),
}
}
@ -141,6 +147,7 @@ impl Stories {
self.tab("story-switch", StoryType::Switch, cx),
self.tab("story-picker", StoryType::Picker, cx),
self.tab("story-dropdown", StoryType::Dropdown, cx),
self.tab("story-tooltip", StoryType::Tooltip, cx),
]))
}
@ -171,6 +178,7 @@ impl Render for Stories {
StoryType::Switch => this.child(self.switch_story.clone()),
StoryType::Picker => this.child(self.picker_story.clone()),
StoryType::Dropdown => this.child(self.dropdown_story.clone()),
StoryType::Tooltip => this.child(self.tooltip_story.clone()),
})
}
}

View file

@ -0,0 +1,67 @@
use gpui::{
div, px, CursorStyle, InteractiveElement, ParentElement, Render, StatefulInteractiveElement,
Styled,
};
use ui::{
button::{Button, ButtonStyle},
checkbox::Checkbox,
h_flex,
label::Label,
tooltip::Tooltip,
v_flex, Selection,
};
use crate::story_case;
pub struct TooltipStory;
impl Render for TooltipStory {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
let description = "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it.";
story_case("Tooltip", description).child(
v_flex()
.w(px(360.))
.gap_5()
.child(
div()
.cursor(CursorStyle::PointingHand)
.child(Button::new("button", "Hover me").style(ButtonStyle::Primary))
.id("tooltip-1")
.tooltip(|cx| Tooltip::text("This is a Button", cx)),
)
.child(
div()
.cursor(CursorStyle::PointingHand)
.child(
Button::new("button-meta", "With meta, Hover me")
.style(ButtonStyle::Primary),
)
.id("tooltip-2")
.tooltip(|cx| {
Tooltip::with_meta("This is a Button", "Click if you want", cx)
}),
)
.child(
h_flex()
.justify_center()
.cursor(CursorStyle::PointingHand)
.child(Label::new("Hover me"))
.id("tooltip-3")
.tooltip(|cx| Tooltip::text("This is a Label", cx)),
)
.child(
div()
.cursor(CursorStyle::PointingHand)
.child(
Checkbox::new("check", cx)
.label("Remember me")
.checked(Selection::Selected),
)
.id("tooltip-4")
.tooltip(|cx| Tooltip::text("Checked!", cx)),
),
)
}
}

View file

@ -5,7 +5,6 @@ use gpui::{
};
use crate::{
label::Label,
span,
theme::{ActiveTheme, Colorize as _, ThemeMode},
Clickable, Disableable, Selectable,

View file

@ -22,6 +22,7 @@ pub mod list;
pub mod picker;
pub mod switch;
pub mod tab;
pub mod tooltip;
pub use clickable::Clickable;
pub use disableable::Disableable;

View file

@ -0,0 +1,157 @@
use std::any::TypeId;
use std::borrow::Cow;
use std::sync::Arc;
use gpui::{
AnyView, DismissEvent, ElementId, Entity, EntityId, EventEmitter, IntoElement, ParentElement,
Render, SharedString, Styled, View, ViewContext, WindowContext,
};
use crate::theme::ActiveTheme;
use crate::{label::Label, v_flex, StyledExt};
#[derive(Debug, PartialEq, Clone)]
pub struct NotificationId {
/// A [`TypeId`] used to uniquely identify this notification.
type_id: TypeId,
/// A supplementary ID used to distinguish between multiple
/// notifications that have the same [`type_id`](Self::type_id);
id: Option<ElementId>,
}
impl NotificationId {
/// Returns a unique [`NotificationId`] for the given type.
pub fn unique<T: 'static>() -> Self {
Self {
type_id: TypeId::of::<T>(),
id: None,
}
}
/// Returns a [`NotificationId`] for the given type that is also identified
/// by the provided ID.
pub fn identified<T: 'static>(id: impl Into<ElementId>) -> Self {
Self {
type_id: TypeId::of::<T>(),
id: Some(id.into()),
}
}
}
// impl Workspace {
// pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
// self.dismiss_notification_internal(id, cx)
// }
// pub fn show_toast(&mut self, toast: MessageNotification, cx: &mut ViewContext<Self>) {
// self.dismiss_notification(&toast.id, cx);
// self.show_notification(toast.id, cx, |cx| {
// cx.new_view(|_cx| match toast.on_click.as_ref() {
// Some((click_msg, on_click)) => {
// let on_click = on_click.clone();
// simple_message_notification::MessageNotification::new(toast.msg.clone())
// .with_click_message(click_msg.clone())
// .on_click(move |cx| on_click(cx))
// }
// None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
// })
// })
// }
// }
pub struct Toast {
id: NotificationId,
msg: Cow<'static, str>,
on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
}
impl Toast {
pub fn new<I: Into<Cow<'static, str>>>(id: NotificationId, msg: I) -> Self {
Toast {
id,
msg: msg.into(),
on_click: None,
}
}
pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
where
M: Into<Cow<'static, str>>,
F: Fn(&mut WindowContext) + 'static,
{
self.on_click = Some((message.into(), Arc::new(on_click)));
self
}
}
impl PartialEq for Toast {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.msg == other.msg
&& self.on_click.is_some() == other.on_click.is_some()
}
}
impl Clone for Toast {
fn clone(&self) -> Self {
Toast {
id: self.id.clone(),
msg: self.msg.clone(),
on_click: self.on_click.clone(),
}
}
}
pub trait Notification: EventEmitter<DismissEvent> + Render {}
impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
pub trait NotificationHandle: Send {
fn id(&self) -> EntityId;
fn to_any(&self) -> AnyView;
}
impl<T: Notification> NotificationHandle for View<T> {
fn id(&self) -> EntityId {
self.entity_id()
}
fn to_any(&self) -> AnyView {
self.clone().into()
}
}
impl From<&dyn NotificationHandle> for AnyView {
fn from(val: &dyn NotificationHandle) -> Self {
val.to_any()
}
}
pub struct MessageNotification {
title: SharedString,
description: SharedString,
}
impl MessageNotification {
pub fn new(title: impl Into<SharedString>, description: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
description: description.into(),
}
}
}
impl Render for MessageNotification {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.elevation_3(cx)
.p_4()
.max_w_80()
.bg(cx.theme().background)
.child(
v_flex()
.child(Label::new(self.title.clone()))
.child(Label::new(self.description.clone())),
)
}
}

View file

@ -1,4 +1,4 @@
use crate::theme::ActiveTheme;
use crate::theme::{ActiveTheme, Colorize};
use gpui::{hsla, point, px, BoxShadow, Styled, WindowContext};
use smallvec::{smallvec, SmallVec};

View file

@ -21,6 +21,7 @@ pub trait Colorize {
fn opacity(&self, opacity: f32) -> Hsla;
fn divide(&self, divisor: f32) -> Hsla;
fn invert(&self) -> Hsla;
fn invert_l(&self) -> Hsla;
fn lighten(&self, amount: f32) -> Hsla;
fn darken(&self, amount: f32) -> Hsla;
}
@ -56,6 +57,14 @@ impl Colorize for Hsla {
}
}
/// Return inverted lightness
fn invert_l(&self) -> Hsla {
Hsla {
l: 1.0 - self.l,
..*self
}
}
fn lighten(&self, amount: f32) -> Hsla {
let l = (self.l + amount).min(1.0);

77
crates/ui/src/tooltip.rs Normal file
View file

@ -0,0 +1,77 @@
use gpui::{
div, prelude::FluentBuilder, px, AnyView, Div, IntoElement, ParentElement, Render,
SharedString, Styled, ViewContext, VisualContext, WindowContext,
};
use crate::{
h_flex,
styled_ext::ElevationIndex,
theme::{ActiveTheme, Colorize},
v_flex,
};
pub struct Tooltip {
title: SharedString,
meta: Option<SharedString>,
}
impl Tooltip {
pub fn text(title: impl Into<SharedString>, cx: &mut WindowContext) -> AnyView {
cx.new_view(|_cx| Self {
title: title.into(),
meta: None,
})
.into()
}
pub fn new(title: impl Into<SharedString>) -> Self {
Self {
title: title.into(),
meta: None,
}
}
pub fn with_meta(
title: impl Into<SharedString>,
meta: impl Into<SharedString>,
cx: &mut WindowContext,
) -> AnyView {
cx.new_view(|_: &mut ViewContext<Tooltip>| Self {
title: title.into(),
meta: Some(meta.into()),
})
.into()
}
// TODO: pub fn for_action
}
impl Render for Tooltip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
tooltip_container(cx, |el, _| {
el.child(h_flex().gap_4().child(self.title.clone()))
.when_some(self.meta.clone(), |this, meta| {
this.child(div().text_size(px(12.)).child(meta))
})
})
}
}
pub fn tooltip_container<V>(
cx: &mut ViewContext<V>,
f: impl FnOnce(Div, &mut ViewContext<V>) -> Div,
) -> impl IntoElement {
// padding to avoid tooltip appearing right below the mouse cursor
div().pl_2().pt_2p5().child(
v_flex()
.bg(cx.theme().popover.invert_l())
.rounded(px(8.))
.border_1()
.border_color(cx.theme().border.invert_l())
.shadow(ElevationIndex::ElevatedSurface.shadow())
.text_color(cx.theme().background)
.py_1()
.px_2()
.map(|el| f(el, cx)),
)
}

View file

@ -18,7 +18,6 @@ mod notification;
pub use app_state::AppState;
pub struct Workspace {
weak_self: WeakView<Self>,
stories: View<Stories>,
notifications: Vec<(NotificationId, Box<dyn NotificationHandle>)>,
}
@ -29,10 +28,7 @@ impl Workspace {
_parent: Option<WeakView<Self>>,
cx: &mut ViewContext<Self>,
) -> Self {
let weak_handle = cx.view().downgrade();
Workspace {
weak_self: weak_handle.clone(),
stories: Stories::view(cx),
notifications: Default::default(),
}