feat: add support for multi languages (#79)
* update backup settings description * add rust-i18n * translate * . * update translations * fix * update translate * .
This commit is contained in:
@@ -1,28 +1,30 @@
|
||||
[package]
|
||||
name = "ui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
theme = { path = "../theme" }
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smallvec.workspace = true
|
||||
anyhow.workspace = true
|
||||
itertools.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
paste = "1"
|
||||
regex = "1"
|
||||
unicode-segmentation = "1.12.0"
|
||||
uuid = "1.10"
|
||||
once_cell = "1.19.0"
|
||||
image = "0.25.1"
|
||||
linkify = "0.10.0"
|
||||
emojis.workspace = true
|
||||
[package]
|
||||
name = "ui"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
theme = { path = "../theme" }
|
||||
|
||||
rust-i18n.workspace = true
|
||||
i18n.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smallvec.workspace = true
|
||||
anyhow.workspace = true
|
||||
itertools.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
paste = "1"
|
||||
regex = "1"
|
||||
unicode-segmentation = "1.12.0"
|
||||
uuid = "1.10"
|
||||
once_cell = "1.19.0"
|
||||
image = "0.25.1"
|
||||
linkify = "0.10.0"
|
||||
emojis.workspace = true
|
||||
|
||||
@@ -1,312 +1,314 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Sizable, Size};
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub enum IconName {
|
||||
AddressBook,
|
||||
ArrowIn,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
ArrowUpCircle,
|
||||
Bell,
|
||||
CaretUp,
|
||||
CaretDown,
|
||||
CaretDownFill,
|
||||
CaretRight,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckCircleFill,
|
||||
Close,
|
||||
CloseCircle,
|
||||
CloseCircleFill,
|
||||
Copy,
|
||||
EditFill,
|
||||
Ellipsis,
|
||||
Eye,
|
||||
EyeOff,
|
||||
EmojiFill,
|
||||
Folder,
|
||||
FolderFill,
|
||||
Filter,
|
||||
FilterFill,
|
||||
Inbox,
|
||||
Info,
|
||||
Loader,
|
||||
Logout,
|
||||
Moon,
|
||||
PanelBottom,
|
||||
PanelBottomOpen,
|
||||
PanelLeft,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
PanelRight,
|
||||
PanelRightClose,
|
||||
PanelRightOpen,
|
||||
Plus,
|
||||
PlusFill,
|
||||
PlusCircleFill,
|
||||
Relays,
|
||||
ResizeCorner,
|
||||
Reply,
|
||||
Forward,
|
||||
Search,
|
||||
SearchFill,
|
||||
Settings,
|
||||
SortAscending,
|
||||
SortDescending,
|
||||
Sun,
|
||||
Toggle,
|
||||
ToggleFill,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
Upload,
|
||||
UsersThreeFill,
|
||||
WindowClose,
|
||||
WindowMaximize,
|
||||
WindowMinimize,
|
||||
WindowRestore,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
pub fn path(self) -> SharedString {
|
||||
match self {
|
||||
Self::AddressBook => "icons/address-book.svg",
|
||||
Self::ArrowIn => "icons/arrows-in.svg",
|
||||
Self::ArrowDown => "icons/arrow-down.svg",
|
||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||
Self::ArrowRight => "icons/arrow-right.svg",
|
||||
Self::ArrowUp => "icons/arrow-up.svg",
|
||||
Self::ArrowUpCircle => "icons/arrow-up-circle.svg",
|
||||
Self::Bell => "icons/bell.svg",
|
||||
Self::CaretRight => "icons/caret-right.svg",
|
||||
Self::CaretUp => "icons/caret-up.svg",
|
||||
Self::CaretDown => "icons/caret-down.svg",
|
||||
Self::CaretDownFill => "icons/caret-down-fill.svg",
|
||||
Self::Check => "icons/check.svg",
|
||||
Self::CheckCircle => "icons/check-circle.svg",
|
||||
Self::CheckCircleFill => "icons/check-circle-fill.svg",
|
||||
Self::Close => "icons/close.svg",
|
||||
Self::CloseCircle => "icons/close-circle.svg",
|
||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||
Self::Copy => "icons/copy.svg",
|
||||
Self::EditFill => "icons/edit-fill.svg",
|
||||
Self::Ellipsis => "icons/ellipsis.svg",
|
||||
Self::Eye => "icons/eye.svg",
|
||||
Self::EmojiFill => "icons/emoji-fill.svg",
|
||||
Self::EyeOff => "icons/eye-off.svg",
|
||||
Self::Folder => "icons/folder.svg",
|
||||
Self::FolderFill => "icons/folder-fill.svg",
|
||||
Self::Filter => "icons/filter.svg",
|
||||
Self::FilterFill => "icons/filter-fill.svg",
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::Loader => "icons/loader.svg",
|
||||
Self::Logout => "icons/logout.svg",
|
||||
Self::Moon => "icons/moon.svg",
|
||||
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||
Self::PanelLeft => "icons/panel-left.svg",
|
||||
Self::PanelLeftClose => "icons/panel-left-close.svg",
|
||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||
Self::PanelRight => "icons/panel-right.svg",
|
||||
Self::PanelRightClose => "icons/panel-right-close.svg",
|
||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||
Self::Plus => "icons/plus.svg",
|
||||
Self::PlusFill => "icons/plus-fill.svg",
|
||||
Self::PlusCircleFill => "icons/plus-circle-fill.svg",
|
||||
Self::Relays => "icons/relays.svg",
|
||||
Self::ResizeCorner => "icons/resize-corner.svg",
|
||||
Self::Reply => "icons/reply.svg",
|
||||
Self::Forward => "icons/forward.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
Self::SearchFill => "icons/search-fill.svg",
|
||||
Self::Settings => "icons/settings.svg",
|
||||
Self::SortAscending => "icons/sort-ascending.svg",
|
||||
Self::SortDescending => "icons/sort-descending.svg",
|
||||
Self::Sun => "icons/sun.svg",
|
||||
Self::Toggle => "icons/toggle.svg",
|
||||
Self::ToggleFill => "icons/toggle-fill.svg",
|
||||
Self::ThumbsDown => "icons/thumbs-down.svg",
|
||||
Self::ThumbsUp => "icons/thumbs-up.svg",
|
||||
Self::Upload => "icons/upload.svg",
|
||||
Self::UsersThreeFill => "icons/users-three-fill.svg",
|
||||
Self::WindowClose => "icons/window-close.svg",
|
||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||
Self::WindowMinimize => "icons/window-minimize.svg",
|
||||
Self::WindowRestore => "icons/window-restore.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Return the icon as a Entity<Icon>
|
||||
pub fn view(self, window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
||||
Icon::build(self).view(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconName> for Icon {
|
||||
fn from(val: IconName) -> Self {
|
||||
Icon::build(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconName> for AnyElement {
|
||||
fn from(val: IconName) -> Self {
|
||||
Icon::build(val).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for IconName {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
Icon::build(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Icon {
|
||||
base: Svg,
|
||||
path: SharedString,
|
||||
text_color: Option<Hsla>,
|
||||
size: Option<Size>,
|
||||
rotation: Option<Radians>,
|
||||
}
|
||||
|
||||
impl Default for Icon {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: svg().flex_none().size_4(),
|
||||
path: "".into(),
|
||||
text_color: None,
|
||||
size: None,
|
||||
rotation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Icon {
|
||||
fn clone(&self) -> Self {
|
||||
let mut this = Self::default().path(self.path.clone());
|
||||
if let Some(size) = self.size {
|
||||
this = this.with_size(size);
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IconNamed {
|
||||
fn path(&self) -> SharedString;
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn new(icon: impl Into<Icon>) -> Self {
|
||||
icon.into()
|
||||
}
|
||||
|
||||
fn build(name: IconName) -> Self {
|
||||
Self::default().path(name.path())
|
||||
}
|
||||
|
||||
/// Set the icon path of the Assets bundle
|
||||
///
|
||||
/// For example: `icons/foo.svg`
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = path.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new view for the icon
|
||||
pub fn view(self, _window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
||||
cx.new(|_| self)
|
||||
}
|
||||
|
||||
pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
|
||||
self.base = self.base.with_transformation(transformation);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Rotate the icon by the given angle
|
||||
pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
|
||||
self.base = self
|
||||
.base
|
||||
.with_transformation(Transformation::rotate(radians));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Icon {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
|
||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.text_color = Some(color.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Icon {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = Some(size.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| window.text_style().color);
|
||||
|
||||
self.base
|
||||
.text_color(text_color)
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_4(),
|
||||
Size::Medium => this.size_5(),
|
||||
Size::Large => this.size_6(),
|
||||
})
|
||||
.path(self.path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Icon> for AnyElement {
|
||||
fn from(val: Icon) -> Self {
|
||||
val.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Icon {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
|
||||
|
||||
svg()
|
||||
.flex_none()
|
||||
.text_color(text_color)
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_4(),
|
||||
Size::Medium => this.size_5(),
|
||||
Size::Large => this.size_6(),
|
||||
})
|
||||
.path(self.path.clone())
|
||||
.when_some(self.rotation, |this, rotation| {
|
||||
this.with_transformation(Transformation::rotate(rotation))
|
||||
})
|
||||
}
|
||||
}
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
svg, AnyElement, App, AppContext, Entity, Hsla, IntoElement, Radians, Render, RenderOnce,
|
||||
SharedString, StyleRefinement, Styled, Svg, Transformation, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{Sizable, Size};
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub enum IconName {
|
||||
AddressBook,
|
||||
ArrowIn,
|
||||
ArrowDown,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
ArrowUpCircle,
|
||||
Bell,
|
||||
CaretUp,
|
||||
CaretDown,
|
||||
CaretDownFill,
|
||||
CaretRight,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckCircleFill,
|
||||
Close,
|
||||
CloseCircle,
|
||||
CloseCircleFill,
|
||||
Copy,
|
||||
EditFill,
|
||||
Ellipsis,
|
||||
Eye,
|
||||
EyeOff,
|
||||
EmojiFill,
|
||||
Folder,
|
||||
FolderFill,
|
||||
Filter,
|
||||
FilterFill,
|
||||
Inbox,
|
||||
Info,
|
||||
Language,
|
||||
Loader,
|
||||
Logout,
|
||||
Moon,
|
||||
PanelBottom,
|
||||
PanelBottomOpen,
|
||||
PanelLeft,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
PanelRight,
|
||||
PanelRightClose,
|
||||
PanelRightOpen,
|
||||
Plus,
|
||||
PlusFill,
|
||||
PlusCircleFill,
|
||||
Relays,
|
||||
ResizeCorner,
|
||||
Reply,
|
||||
Forward,
|
||||
Search,
|
||||
SearchFill,
|
||||
Settings,
|
||||
SortAscending,
|
||||
SortDescending,
|
||||
Sun,
|
||||
Toggle,
|
||||
ToggleFill,
|
||||
ThumbsDown,
|
||||
ThumbsUp,
|
||||
Upload,
|
||||
UsersThreeFill,
|
||||
WindowClose,
|
||||
WindowMaximize,
|
||||
WindowMinimize,
|
||||
WindowRestore,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
pub fn path(self) -> SharedString {
|
||||
match self {
|
||||
Self::AddressBook => "icons/address-book.svg",
|
||||
Self::ArrowIn => "icons/arrows-in.svg",
|
||||
Self::ArrowDown => "icons/arrow-down.svg",
|
||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||
Self::ArrowRight => "icons/arrow-right.svg",
|
||||
Self::ArrowUp => "icons/arrow-up.svg",
|
||||
Self::ArrowUpCircle => "icons/arrow-up-circle.svg",
|
||||
Self::Bell => "icons/bell.svg",
|
||||
Self::CaretRight => "icons/caret-right.svg",
|
||||
Self::CaretUp => "icons/caret-up.svg",
|
||||
Self::CaretDown => "icons/caret-down.svg",
|
||||
Self::CaretDownFill => "icons/caret-down-fill.svg",
|
||||
Self::Check => "icons/check.svg",
|
||||
Self::CheckCircle => "icons/check-circle.svg",
|
||||
Self::CheckCircleFill => "icons/check-circle-fill.svg",
|
||||
Self::Close => "icons/close.svg",
|
||||
Self::CloseCircle => "icons/close-circle.svg",
|
||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||
Self::Copy => "icons/copy.svg",
|
||||
Self::EditFill => "icons/edit-fill.svg",
|
||||
Self::Ellipsis => "icons/ellipsis.svg",
|
||||
Self::Eye => "icons/eye.svg",
|
||||
Self::EmojiFill => "icons/emoji-fill.svg",
|
||||
Self::EyeOff => "icons/eye-off.svg",
|
||||
Self::Folder => "icons/folder.svg",
|
||||
Self::FolderFill => "icons/folder-fill.svg",
|
||||
Self::Filter => "icons/filter.svg",
|
||||
Self::FilterFill => "icons/filter-fill.svg",
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::Language => "icons/language.svg",
|
||||
Self::Loader => "icons/loader.svg",
|
||||
Self::Logout => "icons/logout.svg",
|
||||
Self::Moon => "icons/moon.svg",
|
||||
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||
Self::PanelLeft => "icons/panel-left.svg",
|
||||
Self::PanelLeftClose => "icons/panel-left-close.svg",
|
||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||
Self::PanelRight => "icons/panel-right.svg",
|
||||
Self::PanelRightClose => "icons/panel-right-close.svg",
|
||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||
Self::Plus => "icons/plus.svg",
|
||||
Self::PlusFill => "icons/plus-fill.svg",
|
||||
Self::PlusCircleFill => "icons/plus-circle-fill.svg",
|
||||
Self::Relays => "icons/relays.svg",
|
||||
Self::ResizeCorner => "icons/resize-corner.svg",
|
||||
Self::Reply => "icons/reply.svg",
|
||||
Self::Forward => "icons/forward.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
Self::SearchFill => "icons/search-fill.svg",
|
||||
Self::Settings => "icons/settings.svg",
|
||||
Self::SortAscending => "icons/sort-ascending.svg",
|
||||
Self::SortDescending => "icons/sort-descending.svg",
|
||||
Self::Sun => "icons/sun.svg",
|
||||
Self::Toggle => "icons/toggle.svg",
|
||||
Self::ToggleFill => "icons/toggle-fill.svg",
|
||||
Self::ThumbsDown => "icons/thumbs-down.svg",
|
||||
Self::ThumbsUp => "icons/thumbs-up.svg",
|
||||
Self::Upload => "icons/upload.svg",
|
||||
Self::UsersThreeFill => "icons/users-three-fill.svg",
|
||||
Self::WindowClose => "icons/window-close.svg",
|
||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||
Self::WindowMinimize => "icons/window-minimize.svg",
|
||||
Self::WindowRestore => "icons/window-restore.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Return the icon as a Entity<Icon>
|
||||
pub fn view(self, window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
||||
Icon::build(self).view(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconName> for Icon {
|
||||
fn from(val: IconName) -> Self {
|
||||
Icon::build(val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconName> for AnyElement {
|
||||
fn from(val: IconName) -> Self {
|
||||
Icon::build(val).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for IconName {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
Icon::build(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Icon {
|
||||
base: Svg,
|
||||
path: SharedString,
|
||||
text_color: Option<Hsla>,
|
||||
size: Option<Size>,
|
||||
rotation: Option<Radians>,
|
||||
}
|
||||
|
||||
impl Default for Icon {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base: svg().flex_none().size_4(),
|
||||
path: "".into(),
|
||||
text_color: None,
|
||||
size: None,
|
||||
rotation: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Icon {
|
||||
fn clone(&self) -> Self {
|
||||
let mut this = Self::default().path(self.path.clone());
|
||||
if let Some(size) = self.size {
|
||||
this = this.with_size(size);
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IconNamed {
|
||||
fn path(&self) -> SharedString;
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn new(icon: impl Into<Icon>) -> Self {
|
||||
icon.into()
|
||||
}
|
||||
|
||||
fn build(name: IconName) -> Self {
|
||||
Self::default().path(name.path())
|
||||
}
|
||||
|
||||
/// Set the icon path of the Assets bundle
|
||||
///
|
||||
/// For example: `icons/foo.svg`
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = path.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a new view for the icon
|
||||
pub fn view(self, _window: &mut Window, cx: &mut App) -> Entity<Icon> {
|
||||
cx.new(|_| self)
|
||||
}
|
||||
|
||||
pub fn transform(mut self, transformation: gpui::Transformation) -> Self {
|
||||
self.base = self.base.with_transformation(transformation);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Rotate the icon by the given angle
|
||||
pub fn rotate(mut self, radians: impl Into<Radians>) -> Self {
|
||||
self.base = self
|
||||
.base
|
||||
.with_transformation(Transformation::rotate(radians));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Icon {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.base.style()
|
||||
}
|
||||
|
||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.text_color = Some(color.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Icon {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = Some(size.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| window.text_style().color);
|
||||
|
||||
self.base
|
||||
.text_color(text_color)
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_4(),
|
||||
Size::Medium => this.size_5(),
|
||||
Size::Large => this.size_6(),
|
||||
})
|
||||
.path(self.path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Icon> for AnyElement {
|
||||
fn from(val: Icon) -> Self {
|
||||
val.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Icon {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let text_color = self.text_color.unwrap_or_else(|| cx.theme().icon);
|
||||
|
||||
svg()
|
||||
.flex_none()
|
||||
.text_color(text_color)
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_4(),
|
||||
Size::Medium => this.size_5(),
|
||||
Size::Large => this.size_6(),
|
||||
})
|
||||
.when(!self.path.is_empty(), |this| this.path(self.path.clone()))
|
||||
.when_some(self.rotation, |this, rotation| {
|
||||
this.with_transformation(Transformation::rotate(rotation))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ mod styled;
|
||||
mod title_bar;
|
||||
mod window_border;
|
||||
|
||||
i18n::init!();
|
||||
|
||||
/// Initialize the UI module.
|
||||
///
|
||||
/// This must be called before using any of the UI components.
|
||||
|
||||
@@ -1,382 +1,389 @@
|
||||
use std::any::TypeId;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
blue, div, green, px, red, yellow, Animation, AnimationExt, App, AppContext, ClickEvent,
|
||||
Context, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||
Window,
|
||||
};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
|
||||
|
||||
pub enum NotificationType {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
||||
pub(crate) enum NotificationId {
|
||||
Id(TypeId),
|
||||
IdAndElementId(TypeId, ElementId),
|
||||
}
|
||||
|
||||
impl From<TypeId> for NotificationId {
|
||||
fn from(type_id: TypeId) -> Self {
|
||||
Self::Id(type_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(TypeId, ElementId)> for NotificationId {
|
||||
fn from((type_id, id): (TypeId, ElementId)) -> Self {
|
||||
Self::IdAndElementId(type_id, id)
|
||||
}
|
||||
}
|
||||
|
||||
type OnClick = Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>;
|
||||
|
||||
/// A notification element.
|
||||
pub struct Notification {
|
||||
/// The id is used make the notification unique.
|
||||
/// Then you push a notification with the same id, the previous notification will be replaced.
|
||||
///
|
||||
/// None means the notification will be added to the end of the list.
|
||||
id: NotificationId,
|
||||
kind: NotificationType,
|
||||
title: Option<SharedString>,
|
||||
message: SharedString,
|
||||
icon: Option<Icon>,
|
||||
autohide: bool,
|
||||
on_click: OnClick,
|
||||
closing: bool,
|
||||
}
|
||||
|
||||
impl From<String> for Notification {
|
||||
fn from(s: String) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for Notification {
|
||||
fn from(s: SharedString) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Notification {
|
||||
fn from(s: &'static str) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationType, &'static str)> for Notification {
|
||||
fn from((type_, content): (NotificationType, &'static str)) -> Self {
|
||||
Self::new(content).with_type(type_)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationType, SharedString)> for Notification {
|
||||
fn from((type_, content): (NotificationType, SharedString)) -> Self {
|
||||
Self::new(content).with_type(type_)
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultIdType;
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification with the given content.
|
||||
///
|
||||
/// default width is 320px.
|
||||
pub fn new(message: impl Into<SharedString>) -> Self {
|
||||
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
||||
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: None,
|
||||
message: message.into(),
|
||||
kind: NotificationType::Info,
|
||||
icon: None,
|
||||
autohide: true,
|
||||
on_click: None,
|
||||
closing: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn info(message: impl Into<SharedString>) -> Self {
|
||||
Self::new(message).with_type(NotificationType::Info)
|
||||
}
|
||||
|
||||
pub fn success(message: impl Into<SharedString>) -> Self {
|
||||
Self::new(message).with_type(NotificationType::Success)
|
||||
}
|
||||
|
||||
pub fn warning(message: impl Into<SharedString>) -> Self {
|
||||
Self::new(message).with_type(NotificationType::Warning)
|
||||
}
|
||||
|
||||
pub fn error(message: impl Into<SharedString>) -> Self {
|
||||
Self::new(message).with_type(NotificationType::Error)
|
||||
}
|
||||
|
||||
/// Set the type for unique identification of the notification.
|
||||
///
|
||||
/// ```rs
|
||||
/// struct MyNotificationKind;
|
||||
/// let notification = Notification::new("Hello").id::<MyNotificationKind>();
|
||||
/// ```
|
||||
pub fn id<T: Sized + 'static>(mut self) -> Self {
|
||||
self.id = TypeId::of::<T>().into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the type and id of the notification, used to uniquely identify the notification.
|
||||
pub fn id1<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
|
||||
self.id = (TypeId::of::<T>(), key.into()).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the title of the notification, default is None.
|
||||
///
|
||||
/// If title is None, the notification will not have a title.
|
||||
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icon of the notification.
|
||||
///
|
||||
/// If icon is None, the notification will use the default icon of the type.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the type of the notification, default is NotificationType::Info.
|
||||
pub fn with_type(mut self, type_: NotificationType) -> Self {
|
||||
self.kind = type_;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the auto hide of the notification, default is true.
|
||||
pub fn autohide(mut self, autohide: bool) -> Self {
|
||||
self.autohide = autohide;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click callback of the notification.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Arc::new(on_click));
|
||||
self
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.closing = true;
|
||||
cx.notify();
|
||||
|
||||
// Dismiss the notification after 0.15s to show the animation.
|
||||
cx.spawn(async move |view, cx| {
|
||||
Timer::after(Duration::from_secs_f32(0.15)).await;
|
||||
cx.update(|cx| {
|
||||
if let Some(view) = view.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
view.closing = false;
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for Notification {}
|
||||
|
||||
impl FluentBuilder for Notification {}
|
||||
|
||||
impl Render for Notification {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let closing = self.closing;
|
||||
let icon = match self.icon.clone() {
|
||||
Some(icon) => icon,
|
||||
None => match self.kind {
|
||||
NotificationType::Info => Icon::new(IconName::Info).text_color(blue()),
|
||||
NotificationType::Warning => Icon::new(IconName::Info).text_color(yellow()),
|
||||
NotificationType::Error => Icon::new(IconName::CloseCircle).text_color(red()),
|
||||
NotificationType::Success => Icon::new(IconName::CheckCircle).text_color(green()),
|
||||
},
|
||||
};
|
||||
|
||||
div()
|
||||
.id("notification")
|
||||
.group("")
|
||||
.occlude()
|
||||
.relative()
|
||||
.w_72()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_background)
|
||||
.rounded(cx.theme().radius)
|
||||
.shadow_md()
|
||||
.p_2()
|
||||
.gap_3()
|
||||
.child(div().absolute().top_2p5().left_2().child(icon))
|
||||
.child(
|
||||
v_flex()
|
||||
.pl_6()
|
||||
.gap_1()
|
||||
.when_some(self.title.clone(), |this, title| {
|
||||
this.child(div().text_xs().font_semibold().child(title))
|
||||
})
|
||||
.overflow_hidden()
|
||||
.child(div().text_xs().child(self.message.clone())),
|
||||
)
|
||||
.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.cursor_pointer()
|
||||
.on_click(cx.listener(move |view, event, window, cx| {
|
||||
view.dismiss(event, window, cx);
|
||||
on_click(event, window, cx);
|
||||
}))
|
||||
})
|
||||
.when(!self.autohide, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_1()
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(
|
||||
Button::new("close")
|
||||
.icon(IconName::Close)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(Self::dismiss)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.with_animation(
|
||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.15))
|
||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||
move |this, delta| {
|
||||
if closing {
|
||||
let x_offset = px(0.) + delta * px(45.);
|
||||
this.left(px(0.) + x_offset).opacity(1. - delta)
|
||||
} else {
|
||||
let y_offset = px(-45.) + delta * px(45.);
|
||||
this.top(px(0.) + y_offset).opacity(delta)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of notifications.
|
||||
pub struct NotificationList {
|
||||
/// Notifications that will be auto hidden.
|
||||
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
||||
expanded: bool,
|
||||
subscriptions: HashMap<NotificationId, Subscription>,
|
||||
}
|
||||
|
||||
impl NotificationList {
|
||||
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
notifications: VecDeque::new(),
|
||||
expanded: false,
|
||||
subscriptions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
&mut self,
|
||||
notification: impl Into<Notification>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let notification = notification.into();
|
||||
let id = notification.id.clone();
|
||||
let autohide = notification.autohide;
|
||||
|
||||
// Remove the notification by id, for keep unique.
|
||||
self.notifications.retain(|note| note.read(cx).id != id);
|
||||
|
||||
let notification = cx.new(|_| notification);
|
||||
|
||||
self.subscriptions.insert(
|
||||
id.clone(),
|
||||
cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
|
||||
view.notifications.retain(|note| id != note.read(cx).id);
|
||||
view.subscriptions.remove(&id);
|
||||
}),
|
||||
);
|
||||
|
||||
self.notifications.push_back(notification.clone());
|
||||
if autohide {
|
||||
// Sleep for 3 seconds to autohide the notification
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
Timer::after(Duration::from_secs(3)).await;
|
||||
_ = notification.update_in(cx, |note, window, cx| {
|
||||
note.dismiss(&ClickEvent::default(), window, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.notifications.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn notifications(&self) -> Vec<Entity<Notification>> {
|
||||
self.notifications.iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for NotificationList {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let size = window.viewport_size();
|
||||
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
||||
|
||||
div()
|
||||
.absolute()
|
||||
.flex()
|
||||
.top_4()
|
||||
.bottom_4()
|
||||
.right_4()
|
||||
.justify_end()
|
||||
.child(
|
||||
v_flex()
|
||||
.id("notification-list")
|
||||
.gap_3()
|
||||
.absolute()
|
||||
.relative()
|
||||
.right_0()
|
||||
.h(size.height - px(8.))
|
||||
.children(items)
|
||||
.on_hover(cx.listener(|view, hovered, _window, cx| {
|
||||
view.expanded = *hovered;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
use std::any::TypeId;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
blue, div, green, px, red, yellow, Animation, AnimationExt, App, AppContext, ClickEvent,
|
||||
Context, DismissEvent, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
|
||||
Window,
|
||||
};
|
||||
use smol::Timer;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonVariants as _};
|
||||
use crate::{h_flex, v_flex, Icon, IconName, Sizable as _, StyledExt};
|
||||
|
||||
pub enum NotificationType {
|
||||
Info,
|
||||
Success,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Hash, Eq)]
|
||||
pub(crate) enum NotificationId {
|
||||
Id(TypeId),
|
||||
IdAndElementId(TypeId, ElementId),
|
||||
}
|
||||
|
||||
impl From<TypeId> for NotificationId {
|
||||
fn from(type_id: TypeId) -> Self {
|
||||
Self::Id(type_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(TypeId, ElementId)> for NotificationId {
|
||||
fn from((type_id, id): (TypeId, ElementId)) -> Self {
|
||||
Self::IdAndElementId(type_id, id)
|
||||
}
|
||||
}
|
||||
|
||||
type OnClick = Option<Arc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>;
|
||||
|
||||
/// A notification element.
|
||||
pub struct Notification {
|
||||
/// The id is used make the notification unique.
|
||||
/// Then you push a notification with the same id, the previous notification will be replaced.
|
||||
///
|
||||
/// None means the notification will be added to the end of the list.
|
||||
id: NotificationId,
|
||||
kind: NotificationType,
|
||||
title: Option<SharedString>,
|
||||
message: SharedString,
|
||||
icon: Option<Icon>,
|
||||
autohide: bool,
|
||||
on_click: OnClick,
|
||||
closing: bool,
|
||||
}
|
||||
|
||||
impl From<String> for Notification {
|
||||
fn from(s: String) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'static, str>> for Notification {
|
||||
fn from(s: Cow<'static, str>) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for Notification {
|
||||
fn from(s: SharedString) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Notification {
|
||||
fn from(s: &'static str) -> Self {
|
||||
Self::new(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationType, &'static str)> for Notification {
|
||||
fn from((type_, content): (NotificationType, &'static str)) -> Self {
|
||||
Self::new(content).with_type(type_)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(NotificationType, SharedString)> for Notification {
|
||||
fn from((type_, content): (NotificationType, SharedString)) -> Self {
|
||||
Self::new(content).with_type(type_)
|
||||
}
|
||||
}
|
||||
|
||||
struct DefaultIdType;
|
||||
|
||||
impl Notification {
|
||||
/// Create a new notification with the given content.
|
||||
///
|
||||
/// default width is 320px.
|
||||
pub fn new(message: impl Into<SharedString>) -> Self {
|
||||
let id: SharedString = uuid::Uuid::new_v4().to_string().into();
|
||||
let id = (TypeId::of::<DefaultIdType>(), id.into());
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
title: None,
|
||||
message: message.into(),
|
||||
kind: NotificationType::Info,
|
||||
icon: None,
|
||||
autohide: true,
|
||||
on_click: None,
|
||||
closing: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn info(message: impl Into<SharedString>) -> Self {
|
||||
Self::new(message).with_type(NotificationType::Info)
|
||||
}
|
||||
|
||||
pub fn success(message: impl Into<SharedString>) -> Self {
|
||||
Self::new(message).with_type(NotificationType::Success)
|
||||
}
|
||||
|
||||
pub fn warning(message: impl Into<SharedString>) -> Self {
|
||||
Self::new(message).with_type(NotificationType::Warning)
|
||||
}
|
||||
|
||||
pub fn error(message: impl Into<SharedString>) -> Self {
|
||||
Self::new(message).with_type(NotificationType::Error)
|
||||
}
|
||||
|
||||
/// Set the type for unique identification of the notification.
|
||||
///
|
||||
/// ```rs
|
||||
/// struct MyNotificationKind;
|
||||
/// let notification = Notification::new("Hello").id::<MyNotificationKind>();
|
||||
/// ```
|
||||
pub fn id<T: Sized + 'static>(mut self) -> Self {
|
||||
self.id = TypeId::of::<T>().into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the type and id of the notification, used to uniquely identify the notification.
|
||||
pub fn id1<T: Sized + 'static>(mut self, key: impl Into<ElementId>) -> Self {
|
||||
self.id = (TypeId::of::<T>(), key.into()).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the title of the notification, default is None.
|
||||
///
|
||||
/// If title is None, the notification will not have a title.
|
||||
pub fn title(mut self, title: impl Into<SharedString>) -> Self {
|
||||
self.title = Some(title.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the icon of the notification.
|
||||
///
|
||||
/// If icon is None, the notification will use the default icon of the type.
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the type of the notification, default is NotificationType::Info.
|
||||
pub fn with_type(mut self, type_: NotificationType) -> Self {
|
||||
self.kind = type_;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the auto hide of the notification, default is true.
|
||||
pub fn autohide(mut self, autohide: bool) -> Self {
|
||||
self.autohide = autohide;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click callback of the notification.
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Arc::new(on_click));
|
||||
self
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.closing = true;
|
||||
cx.notify();
|
||||
|
||||
// Dismiss the notification after 0.15s to show the animation.
|
||||
cx.spawn(async move |view, cx| {
|
||||
Timer::after(Duration::from_secs_f32(0.15)).await;
|
||||
cx.update(|cx| {
|
||||
if let Some(view) = view.upgrade() {
|
||||
view.update(cx, |view, cx| {
|
||||
view.closing = false;
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for Notification {}
|
||||
|
||||
impl FluentBuilder for Notification {}
|
||||
|
||||
impl Render for Notification {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let closing = self.closing;
|
||||
let icon = match self.icon.clone() {
|
||||
Some(icon) => icon,
|
||||
None => match self.kind {
|
||||
NotificationType::Info => Icon::new(IconName::Info).text_color(blue()),
|
||||
NotificationType::Warning => Icon::new(IconName::Info).text_color(yellow()),
|
||||
NotificationType::Error => Icon::new(IconName::CloseCircle).text_color(red()),
|
||||
NotificationType::Success => Icon::new(IconName::CheckCircle).text_color(green()),
|
||||
},
|
||||
};
|
||||
|
||||
div()
|
||||
.id("notification")
|
||||
.group("")
|
||||
.occlude()
|
||||
.relative()
|
||||
.w_72()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().surface_background)
|
||||
.rounded(cx.theme().radius)
|
||||
.shadow_md()
|
||||
.p_2()
|
||||
.gap_3()
|
||||
.child(div().absolute().top_2p5().left_2().child(icon))
|
||||
.child(
|
||||
v_flex()
|
||||
.pl_6()
|
||||
.gap_1()
|
||||
.when_some(self.title.clone(), |this, title| {
|
||||
this.child(div().text_xs().font_semibold().child(title))
|
||||
})
|
||||
.overflow_hidden()
|
||||
.child(div().text_xs().child(self.message.clone())),
|
||||
)
|
||||
.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.cursor_pointer()
|
||||
.on_click(cx.listener(move |view, event, window, cx| {
|
||||
view.dismiss(event, window, cx);
|
||||
on_click(event, window, cx);
|
||||
}))
|
||||
})
|
||||
.when(!self.autohide, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_1()
|
||||
.invisible()
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(
|
||||
Button::new("close")
|
||||
.icon(IconName::Close)
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(Self::dismiss)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.with_animation(
|
||||
ElementId::NamedInteger("slide-down".into(), closing as u64),
|
||||
Animation::new(Duration::from_secs_f64(0.15))
|
||||
.with_easing(cubic_bezier(0.4, 0., 0.2, 1.)),
|
||||
move |this, delta| {
|
||||
if closing {
|
||||
let x_offset = px(0.) + delta * px(45.);
|
||||
this.left(px(0.) + x_offset).opacity(1. - delta)
|
||||
} else {
|
||||
let y_offset = px(-45.) + delta * px(45.);
|
||||
this.top(px(0.) + y_offset).opacity(delta)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of notifications.
|
||||
pub struct NotificationList {
|
||||
/// Notifications that will be auto hidden.
|
||||
pub(crate) notifications: VecDeque<Entity<Notification>>,
|
||||
expanded: bool,
|
||||
subscriptions: HashMap<NotificationId, Subscription>,
|
||||
}
|
||||
|
||||
impl NotificationList {
|
||||
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
notifications: VecDeque::new(),
|
||||
expanded: false,
|
||||
subscriptions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(
|
||||
&mut self,
|
||||
notification: impl Into<Notification>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let notification = notification.into();
|
||||
let id = notification.id.clone();
|
||||
let autohide = notification.autohide;
|
||||
|
||||
// Remove the notification by id, for keep unique.
|
||||
self.notifications.retain(|note| note.read(cx).id != id);
|
||||
|
||||
let notification = cx.new(|_| notification);
|
||||
|
||||
self.subscriptions.insert(
|
||||
id.clone(),
|
||||
cx.subscribe(¬ification, move |view, _, _: &DismissEvent, cx| {
|
||||
view.notifications.retain(|note| id != note.read(cx).id);
|
||||
view.subscriptions.remove(&id);
|
||||
}),
|
||||
);
|
||||
|
||||
self.notifications.push_back(notification.clone());
|
||||
if autohide {
|
||||
// Sleep for 3 seconds to autohide the notification
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
Timer::after(Duration::from_secs(3)).await;
|
||||
_ = notification.update_in(cx, |note, window, cx| {
|
||||
note.dismiss(&ClickEvent::default(), window, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.notifications.clear();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn notifications(&self) -> Vec<Entity<Notification>> {
|
||||
self.notifications.iter().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for NotificationList {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let size = window.viewport_size();
|
||||
let items = self.notifications.iter().rev().take(10).rev().cloned();
|
||||
|
||||
div()
|
||||
.absolute()
|
||||
.flex()
|
||||
.top_4()
|
||||
.bottom_4()
|
||||
.right_4()
|
||||
.justify_end()
|
||||
.child(
|
||||
v_flex()
|
||||
.id("notification-list")
|
||||
.gap_3()
|
||||
.absolute()
|
||||
.relative()
|
||||
.right_0()
|
||||
.h(size.height - px(8.))
|
||||
.children(items)
|
||||
.on_hover(cx.listener(|view, hovered, _window, cx| {
|
||||
view.expanded = *hovered;
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user