Add Tooltip (#5)
This commit is contained in:
parent
11901dc1e4
commit
a5aea36f75
11 changed files with 324 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
crates/ui-story/src/tooltip_stroy.rs
Normal file
67
crates/ui-story/src/tooltip_stroy.rs
Normal 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)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ use gpui::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
label::Label,
|
||||
span,
|
||||
theme::{ActiveTheme, Colorize as _, ThemeMode},
|
||||
Clickable, Disableable, Selectable,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
157
crates/ui/src/notification.rs
Normal file
157
crates/ui/src/notification.rs
Normal 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())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
77
crates/ui/src/tooltip.rs
Normal 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)),
|
||||
)
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue