refactor ui (#17)

* wip: redesign sidebar

* wip: adjust dpi

* update

* update

* refactor modal

* fix modal
This commit is contained in:
reya
2025-04-18 13:43:07 +07:00
committed by GitHub
parent 5c5748a80c
commit a30f2dcc8a
58 changed files with 899 additions and 1167 deletions

View File

@@ -152,7 +152,9 @@ impl Room {
/// The Profile of the first member in the room
pub fn first_member(&self, cx: &App) -> Profile {
let account = Account::global(cx).read(cx);
let profile = account.profile.clone().unwrap();
let Some(profile) = account.profile.clone() else {
return self.profile_by_pubkey(&self.members[0], cx);
};
if let Some(public_key) = self
.members

View File

@@ -15,7 +15,7 @@ impl SharedProfile for Profile {
.filter(|picture| !picture.is_empty())
.map(|picture| {
format!(
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1",
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1&default=npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445.blossom.band/c30703b48f511c293a9003be8100cdad37b8798b77a1dc3ec6eb8a20443d5dea.png",
IMAGE_SERVICE, picture
)
.into()

View File

@@ -1,31 +1,47 @@
use account::Account;
use common::profile::SharedProfile;
use global::get_client;
use gpui::{
actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription,
Task, Window,
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Window,
};
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use std::sync::Arc;
use ui::{
button::{Button, ButtonRounded, ButtonVariants},
button::{Button, ButtonVariants},
dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem},
popup_menu::PopupMenuExt,
theme::{scale::ColorScaleStep, ActiveTheme, Appearance, Theme},
ContextModal, Icon, IconName, Root, Sizable, TitleBar,
theme::{ActiveTheme, Appearance, Theme},
ContextModal, IconName, Root, Sizable, TitleBar,
};
use crate::views::{chat, contacts, profile, relays, settings, welcome};
use crate::views::{chat, compose, contacts, profile, relays, welcome};
use crate::views::{onboarding, sidebar};
const MODAL_WIDTH: f32 = 420.;
const SIDEBAR_WIDTH: f32 = 280.;
impl_internal_actions!(dock, [AddPanel, ToggleModal]);
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
ChatSpace::new(window, cx)
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub enum PanelKind {
Room(u64),
// More kind will be added here
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub enum ModalKind {
Profile,
Contacts,
Settings,
Compose,
Contact,
Relay,
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct ToggleModal {
pub modal: ModalKind,
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
@@ -40,16 +56,6 @@ impl AddPanel {
}
}
// Dock actions
impl_internal_actions!(dock, [AddPanel]);
// Account actions
actions!(account, [Logout]);
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
ChatSpace::new(window, cx)
}
pub struct ChatSpace {
titlebar: bool,
dock: Entity<DockArea>,
@@ -61,23 +67,26 @@ impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let account = Account::global(cx);
let dock = cx.new(|cx| DockArea::new(window, cx));
let titlebar = false;
cx.new(|cx| {
let mut subscriptions = smallvec![];
subscriptions.push(cx.observe_in(
&account,
window,
|this: &mut ChatSpace, account, window, cx| {
if account.read(cx).profile.is_some() {
this.open_chats(window, cx);
} else {
this.open_onboarding(window, cx);
}
},
));
let mut this = Self {
dock,
titlebar,
subscriptions: smallvec![cx.observe_in(
&account,
window,
|this: &mut ChatSpace, account, window, cx| {
if account.read(cx).profile.is_some() {
this.open_chats(window, cx);
} else {
this.open_onboarding(window, cx);
}
},
)],
subscriptions,
titlebar: false,
};
if Account::global(cx).read(cx).profile.is_some() {
@@ -135,7 +144,7 @@ impl ChatSpace {
);
self.dock.update(cx, |this, cx| {
this.set_left_dock(left, Some(px(260.)), true, window, cx);
this.set_left_dock(left, Some(px(SIDEBAR_WIDTH)), true, window, cx);
this.set_center(center, window, cx);
});
}
@@ -165,74 +174,6 @@ impl ChatSpace {
}))
}
fn render_account_btn(&self, cx: &mut Context<Self>) -> impl IntoElement {
Button::new("account")
.ghost()
.xsmall()
.reverse()
.icon(Icon::new(IconName::ChevronDownSmall))
.when_some(
Account::global(cx).read(cx).profile.as_ref(),
|this, profile| this.child(img(profile.shared_avatar()).size_5()),
)
.popup_menu(move |this, _, _cx| {
this.menu(
"Profile",
Box::new(AddPanel::new(PanelKind::Profile, DockPlacement::Right)),
)
.menu(
"Contacts",
Box::new(AddPanel::new(PanelKind::Contacts, DockPlacement::Right)),
)
.menu(
"Settings",
Box::new(AddPanel::new(PanelKind::Settings, DockPlacement::Center)),
)
.separator()
.menu("Change account", Box::new(Logout))
})
}
fn render_relays_btn(&self, cx: &mut Context<Self>) -> impl IntoElement {
Button::new("relays")
.xsmall()
.ghost()
.icon(IconName::Relays)
.on_click(cx.listener(|this, _, window, cx| {
this.render_edit_relays(window, cx);
}))
}
fn render_edit_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
let relays = relays::init(window, cx);
window.open_modal(cx, move |this, window, cx| {
let is_loading = relays.read(cx).loading();
this.width(px(420.))
.title("Edit your Messaging Relays")
.child(relays.clone())
.footer(
div()
.p_2()
.border_t_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
.child(
Button::new("update_inbox_relays_btn")
.label("Update")
.primary()
.bold()
.rounded(ButtonRounded::Large)
.w_full()
.loading(is_loading)
.on_click(window.listener_for(&relays, |this, _, window, cx| {
this.update(window, cx);
})),
),
)
});
}
fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context<Self>) {
match &action.panel {
PanelKind::Room(id) => {
@@ -246,49 +187,55 @@ impl ChatSpace {
Err(e) => window.push_notification(e.to_string(), cx),
}
}
PanelKind::Profile => {
let panel = profile::init(window, cx);
self.dock.update(cx, |dock_area, cx| {
dock_area.add_panel(panel, action.position, window, cx);
});
}
PanelKind::Contacts => {
let panel = Arc::new(contacts::init(window, cx));
self.dock.update(cx, |dock_area, cx| {
dock_area.add_panel(panel, action.position, window, cx);
});
}
PanelKind::Settings => {
let panel = Arc::new(settings::init(window, cx));
self.dock.update(cx, |dock_area, cx| {
dock_area.add_panel(panel, action.position, window, cx);
});
}
};
}
fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let reset: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
client.reset().await;
Ok(())
});
fn on_modal_action(
&mut self,
action: &ToggleModal,
window: &mut Window,
cx: &mut Context<Self>,
) {
match action.modal {
ModalKind::Profile => {
let profile = profile::init(window, cx);
cx.spawn_in(window, async move |_, cx| {
if reset.await.is_ok() {
cx.update(|_, cx| {
Account::global(cx).update(cx, |this, cx| {
this.profile = None;
cx.notify();
});
window.open_modal(cx, move |modal, _, _| {
modal
.title("Profile")
.width(px(MODAL_WIDTH))
.child(profile.clone())
})
.ok();
};
})
.detach();
}
ModalKind::Compose => {
let compose = compose::init(window, cx);
window.open_modal(cx, move |modal, _, _| {
modal
.title("Direct Messages")
.width(px(MODAL_WIDTH))
.child(compose.clone())
})
}
ModalKind::Contact => {
let contacts = contacts::init(window, cx);
window.open_modal(cx, move |this, _window, _cx| {
this.width(px(MODAL_WIDTH))
.title("Contacts")
.child(contacts.clone())
});
}
ModalKind::Relay => {
let relays = relays::init(window, cx);
window.open_modal(cx, move |this, _, _| {
this.width(px(MODAL_WIDTH))
.title("Edit your Messaging Relays")
.child(relays.clone())
});
}
};
}
}
@@ -319,9 +266,7 @@ impl Render for ChatSpace {
.justify_end()
.gap_2()
.px_2()
.child(self.render_appearance_btn(cx))
.child(self.render_relays_btn(cx))
.child(self.render_account_btn(cx)),
.child(self.render_appearance_btn(cx)),
),
)
})
@@ -334,6 +279,6 @@ impl Render for ChatSpace {
.children(modal_layer)
// Actions
.on_action(cx.listener(Self::on_panel_action))
.on_action(cx.listener(Self::on_logout_action))
.on_action(cx.listener(Self::on_modal_action))
}
}

View File

@@ -261,7 +261,7 @@ fn main() {
}),
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(900.0), px(680.0)),
size(px(920.0), px(700.0)),
cx,
))),
#[cfg(target_os = "linux")]

View File

@@ -14,14 +14,14 @@ use smallvec::{smallvec, SmallVec};
use smol::fs;
use std::{collections::HashMap, sync::Arc};
use ui::{
button::{Button, ButtonRounded, ButtonVariants},
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
input::{InputEvent, TextInput},
notification::Notification,
popup_menu::PopupMenu,
text::RichText,
theme::{scale::ColorScaleStep, ActiveTheme},
v_flex, ContextModal, Icon, IconName, Sizable, StyledExt,
v_flex, ContextModal, Disableable, Icon, IconName, Size, StyledExt,
};
const ALERT: &str = "has not set up Messaging (DM) Relays, so they will NOT receive your messages.";
@@ -58,8 +58,7 @@ impl Chat {
let attaches = cx.new(|_| None);
let input = cx.new(|cx| {
TextInput::new(window, cx)
.appearance(false)
.text_size(ui::Size::Small)
.text_size(Size::Small)
.placeholder("Message...")
});
@@ -343,11 +342,12 @@ impl Chat {
div()
.group("")
.w_full()
.relative()
.flex()
.gap_3()
.w_full()
.p_2()
.px_3()
.py_2()
.map(|this| match message {
RoomMessage::User(item) => {
let text = text_data
@@ -355,6 +355,7 @@ impl Chat {
.or_insert_with(|| RichText::new(item.content.to_owned(), &item.mentions));
this.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
.text_sm()
.child(
div()
.absolute()
@@ -379,19 +380,18 @@ impl Chat {
.flex()
.items_baseline()
.gap_2()
.text_xs()
.child(
div().font_semibold().child(item.author.shared_name()),
)
.child(div().child(item.ago()).text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)),
.child(
div()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::NINE),
)
.child(item.ago()),
),
)
.child(div().text_sm().child(text.element(
"body".into(),
window,
cx,
))),
.child(text.element("body".into(), window, cx)),
)
}
RoomMessage::System(content) => this
@@ -407,7 +407,7 @@ impl Chat {
.group_hover("", |this| this.bg(cx.theme().danger)),
)
.child(img("brand/avatar.png").size_8().flex_shrink_0())
.text_xs()
.text_sm()
.text_color(cx.theme().danger)
.child(content.clone()),
RoomMessage::Announcement => this
@@ -419,12 +419,12 @@ impl Chat {
.justify_center()
.text_center()
.text_xs()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
.line_height(relative(1.3))
.child(
svg()
.path("brand/coop.svg")
.size_8()
.size_10()
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
)
.child(ROOM_DESCRIPTION),
@@ -444,7 +444,7 @@ impl Panel for Chat {
div()
.flex()
.items_center()
.gap_1()
.gap_1p5()
.child(
div()
.flex()
@@ -459,7 +459,7 @@ impl Panel for Chat {
.map(|(ix, facepill)| {
div()
.when(ix > 0, |div| div.ml_neg_1())
.child(img(facepill).size_4())
.child(img(facepill).size_5())
}),
),
)
@@ -491,7 +491,7 @@ impl Render for Chat {
.size_full()
.child(list(self.list_state.clone()).flex_1())
.child(
div().flex_shrink_0().p_2().child(
div().flex_shrink_0().px_3().py_2().child(
div()
.flex()
.flex_col()
@@ -539,7 +539,6 @@ impl Render for Chat {
.child(
div()
.w_full()
.h_9()
.flex()
.items_center()
.gap_2()
@@ -550,30 +549,10 @@ impl Render for Chat {
.on_click(cx.listener(move |this, _, window, cx| {
this.upload_media(window, cx);
}))
.disabled(self.is_uploading)
.loading(self.is_uploading),
)
.child(
div()
.flex_1()
.flex()
.items_center()
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.rounded(px(cx.theme().radius))
.pl_2()
.pr_1()
.child(self.input.clone())
.child(
Button::new("send")
.ghost()
.xsmall()
.bold()
.rounded(ButtonRounded::Medium)
.label("SEND")
.on_click(cx.listener(|this, _, window, cx| {
this.send_message(window, cx)
})),
),
),
.child(self.input.clone()),
),
),
)

View File

@@ -7,9 +7,9 @@ use common::{profile::SharedProfile, random_name};
use global::get_client;
use gpui::{
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
AppContext, ClickEvent, Context, Div, Entity, FocusHandle, InteractiveElement, IntoElement,
ParentElement, Render, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
Subscription, Task, TextAlign, Window,
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign,
Window,
};
use nostr_sdk::prelude::*;
use serde::Deserialize;
@@ -17,18 +17,18 @@ use smallvec::{smallvec, SmallVec};
use smol::Timer;
use std::{
collections::{BTreeSet, HashSet},
rc::Rc,
time::Duration,
};
use ui::{
button::{Button, ButtonRounded},
button::{Button, ButtonVariants},
input::{InputEvent, TextInput},
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Icon, IconName, Sizable, Size, StyledExt,
ContextModal, Disableable, Icon, IconName, Sizable, Size, StyledExt,
};
const DESCRIPTION: &str =
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
cx.new(|cx| Compose::new(window, cx))
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
struct SelectContact(PublicKey);
@@ -58,7 +58,7 @@ impl Compose {
let name = random_name(2);
let mut input = TextInput::new(window, cx)
.appearance(false)
.text_size(Size::XSmall);
.text_size(Size::Small);
input.set_placeholder("Family... . (Optional)");
input.set_text(name, window, cx);
@@ -193,18 +193,6 @@ impl Compose {
.detach();
}
pub fn label(&self, _window: &Window, cx: &App) -> SharedString {
if self.selected.read(cx).len() > 1 {
"Create Group DM".into()
} else {
"Create DM".into()
}
}
pub fn is_submitting(&self) -> bool {
self.is_submitting
}
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let content = self.user_input.read(cx).text().to_string();
@@ -336,6 +324,15 @@ impl Compose {
impl Render for Compose {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
const DESCRIPTION: &str =
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
let label: SharedString = if self.selected.read(cx).len() > 1 {
"Create Group DM".into()
} else {
"Create DM".into()
};
div()
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::on_action_select))
@@ -344,15 +341,13 @@ impl Render for Compose {
.gap_1()
.child(
div()
.px_2()
.text_xs()
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(DESCRIPTION),
)
.when_some(self.error_message.read(cx).as_ref(), |this, msg| {
this.child(
div()
.px_2()
.text_xs()
.text_color(cx.theme().danger)
.child(msg.clone()),
@@ -362,13 +357,12 @@ impl Render for Compose {
div().flex().flex_col().child(
div()
.h_10()
.px_2()
.border_b_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
.flex()
.items_center()
.gap_1()
.child(div().text_xs().font_semibold().child("Title:"))
.child(div().pb_0p5().text_sm().font_semibold().child("Title:"))
.child(self.title_input.clone()),
),
)
@@ -377,25 +371,9 @@ impl Render for Compose {
.flex()
.flex_col()
.gap_2()
.child(div().px_2().text_xs().font_semibold().child("To:"))
.child(
div()
.flex()
.items_center()
.gap_2()
.px_2()
.child(
Button::new("add_user_to_compose_btn")
.icon(IconName::Plus)
.small()
.rounded(ButtonRounded::Size(px(9999.)))
.loading(self.is_loading)
.on_click(cx.listener(|this, _, window, cx| {
this.add(window, cx);
})),
)
.child(self.user_input.clone()),
)
.mt_1()
.child(div().text_sm().font_semibold().child("To:"))
.child(self.user_input.clone())
.map(|this| {
let contacts = self.contacts.read(cx).clone();
let view = cx.entity();
@@ -444,30 +422,35 @@ impl Render for Compose {
div()
.id(ix)
.w_full()
.h_9()
.h_10()
.px_2()
.flex()
.items_center()
.justify_between()
.rounded(px(cx.theme().radius))
.child(
div()
.flex()
.items_center()
.gap_2()
.text_xs()
.child(div().flex_shrink_0().child(
img(item.shared_avatar()).size_6(),
))
.gap_3()
.text_sm()
.child(
img(item.shared_avatar())
.size_7()
.flex_shrink_0(),
)
.child(item.shared_name()),
)
.when(is_select, |this| {
this.child(
Icon::new(IconName::CircleCheck)
.size_3()
.text_color(cx.theme().base.step(
cx,
ColorScaleStep::TWELVE,
)),
Icon::new(IconName::CheckCircleFill)
.small()
.text_color(
cx.theme().accent.step(
cx,
ColorScaleStep::NINE,
),
),
)
})
.hover(|this| {
@@ -490,71 +473,22 @@ impl Render for Compose {
items
},
)
.min_h(px(250.)),
.pb_4()
.min_h(px(280.)),
)
}
}),
)
}
}
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
#[derive(IntoElement)]
pub struct ComposeButton {
base: Div,
label: SharedString,
handler: Handler,
}
impl ComposeButton {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
base: div(),
label: label.into(),
handler: Rc::new(|_, _, _| {}),
}
}
pub fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.handler = Rc::new(handler);
self
}
}
impl RenderOnce for ComposeButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let handler = self.handler.clone();
self.base
.id("compose")
.flex()
.items_center()
.gap_2()
.px_1()
.h_7()
.text_xs()
.font_semibold()
.rounded(px(cx.theme().radius))
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.child(
div()
.size_6()
.flex()
.items_center()
.justify_center()
.rounded_full()
.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
.child(
Icon::new(IconName::ComposeFill)
.small()
.text_color(cx.theme().base.darken(cx)),
),
div().mt_2().child(
Button::new("create_dm_btn")
.label(label)
.primary()
.w_full()
.loading(self.is_submitting)
.disabled(self.is_submitting)
.on_click(cx.listener(|this, _, window, cx| this.compose(window, cx))),
),
)
.child(self.label.clone())
.on_click(move |ev, window, cx| handler(ev, window, cx))
}
}

View File

@@ -19,6 +19,8 @@ use ui::{
Sizable,
};
const MIN_HEIGHT: f32 = 280.;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Contacts> {
Contacts::new(window, cx)
}
@@ -108,51 +110,56 @@ impl Focusable for Contacts {
impl Render for Contacts {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
div().size_full().pt_2().px_2().map(|this| {
let entity = cx.entity().clone();
div().map(|this| {
if let Some(contacts) = self.contacts.clone() {
this.child(
uniform_list(
cx.entity().clone(),
entity,
"contacts",
contacts.len(),
move |_, range, _window, cx| {
let mut items = Vec::new();
let mut items = Vec::with_capacity(contacts.len());
for ix in range {
let item = contacts.get(ix).unwrap().clone();
items.push(
div()
.w_full()
.h_9()
.px_2()
.flex()
.items_center()
.justify_between()
.rounded(px(cx.theme().radius))
.child(
div()
.flex()
.items_center()
.gap_2()
.text_xs()
.child(
div()
.flex_shrink_0()
.child(img(item.shared_avatar()).size_6()),
)
.child(item.shared_name()),
)
.hover(|this| {
this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
}),
);
if let Some(item) = contacts.get(ix) {
items.push(
div()
.w_full()
.h_9()
.px_2()
.flex()
.items_center()
.justify_between()
.rounded(px(cx.theme().radius))
.child(
div()
.flex()
.items_center()
.gap_2()
.text_xs()
.child(
div().flex_shrink_0().child(
img(item.shared_avatar()).size_6(),
),
)
.child(item.shared_name()),
)
.hover(|this| {
this.bg(cx
.theme()
.base
.step(cx, ColorScaleStep::THREE))
}),
);
}
}
items
},
)
.h_full(),
.min_h(px(MIN_HEIGHT)),
)
} else {
this.flex()

View File

@@ -17,13 +17,9 @@ use ui::{
notification::Notification,
popup_menu::PopupMenu,
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, Icon, IconName, Sizable, Size, StyledExt,
ContextModal, Disableable, Sizable, Size, StyledExt,
};
use crate::chat_space::ChatSpace;
use super::onboarding;
const INPUT_INVALID: &str = "You must provide a valid Private Key or Bunker.";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
@@ -60,12 +56,12 @@ impl Login {
let key_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::XSmall)
.text_size(Size::Small)
.placeholder("nsec... or bunker://...")
});
let connect_relay = cx.new(|cx| {
let mut input = TextInput::new(window, cx).text_size(Size::XSmall).small();
let mut input = TextInput::new(window, cx).text_size(Size::Small).small();
input.set_text("wss://relay.nsec.app", window, cx);
input
});
@@ -233,11 +229,6 @@ impl Login {
self.is_logging_in = status;
cx.notify();
}
fn back(&self, window: &mut Window, cx: &mut Context<Self>) {
let panel = onboarding::init(window, cx);
ChatSpace::set_center_panel(panel, window, cx);
}
}
impl Panel for Login {
@@ -299,14 +290,13 @@ impl Render for Login {
.child(
div()
.text_center()
.text_lg()
.text_xl()
.font_semibold()
.line_height(relative(1.2))
.line_height(relative(1.3))
.child("Welcome Back!"),
)
.child(
div()
.text_sm()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
@@ -365,7 +355,6 @@ impl Render for Login {
.text_center()
.child(
div()
.text_sm()
.font_semibold()
.line_height(relative(1.2))
.text_color(
@@ -375,7 +364,7 @@ impl Render for Login {
)
.child(
div()
.text_xs()
.text_sm()
.text_color(
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
)
@@ -424,17 +413,5 @@ impl Render for Login {
),
),
)
.child(
div().absolute().left_2().top_10().w_16().child(
Button::new("back")
.label("Back")
.icon(Icon::new(IconName::ArrowLeft))
.ghost()
.small()
.on_click(cx.listener(move |this, _, window, cx| {
this.back(window, cx);
})),
),
)
}
}

View File

@@ -1,10 +1,10 @@
pub mod chat;
pub mod compose;
pub mod contacts;
pub mod login;
pub mod new_account;
pub mod onboarding;
pub mod profile;
pub mod relays;
pub mod settings;
pub mod sidebar;
pub mod welcome;

View File

@@ -19,10 +19,6 @@ use ui::{
Disableable, Icon, IconName, Sizable, Size, StyledExt,
};
use crate::chat_space::ChatSpace;
use super::onboarding;
const STEAM_ID_DESCRIPTION: &str =
"Steam ID is used to get your currently playing game and update your status.";
@@ -52,26 +48,26 @@ impl NewAccount {
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::XSmall)
.text_size(Size::Small)
.placeholder("Alice")
});
let avatar_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::XSmall)
.text_size(Size::Small)
.small()
.placeholder("https://example.com/avatar.jpg")
});
let steam_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::XSmall)
.text_size(Size::Small)
.placeholder("76561199810385277")
});
let bio_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::XSmall)
.text_size(Size::Small)
.multi_line()
.placeholder("A short introduce about you.")
});
@@ -188,11 +184,6 @@ impl NewAccount {
self.is_uploading = status;
cx.notify();
}
fn back(&self, window: &mut Window, cx: &mut Context<Self>) {
let panel = onboarding::init(window, cx);
ChatSpace::set_center_panel(panel, window, cx);
}
}
impl Panel for NewAccount {
@@ -238,13 +229,13 @@ impl Render for NewAccount {
.flex_col()
.items_center()
.justify_center()
.gap_8()
.gap_10()
.child(
div()
.text_center()
.text_lg()
.font_semibold()
.line_height(relative(1.2))
.line_height(relative(1.3))
.child("Create New Account"),
)
.child(
@@ -295,7 +286,7 @@ impl Render for NewAccount {
.flex()
.flex_col()
.gap_1()
.text_xs()
.text_sm()
.child("Name *:")
.child(self.name_input.clone()),
)
@@ -304,7 +295,7 @@ impl Render for NewAccount {
.flex()
.flex_col()
.gap_1()
.text_xs()
.text_sm()
.child("Bio:")
.child(self.bio_input.clone()),
)
@@ -313,7 +304,7 @@ impl Render for NewAccount {
.flex()
.flex_col()
.gap_1()
.text_xs()
.text_sm()
.child("Steam ID:")
.child(self.steam_input.clone())
.child(
@@ -341,17 +332,5 @@ impl Render for NewAccount {
})),
),
)
.child(
div().absolute().left_2().top_10().w_16().child(
Button::new("back")
.label("Back")
.icon(Icon::new(IconName::ArrowLeft))
.ghost()
.small()
.on_click(cx.listener(move |this, _, window, cx| {
this.back(window, cx);
})),
),
)
}
}

View File

@@ -16,7 +16,7 @@ use super::{login, new_account};
const LOGO_URL: &str = "brand/coop.svg";
const TITLE: &str = "Welcome to Coop!";
const SUBTITLE: &str = "a Nostr client for secure communication.";
const SUBTITLE: &str = "Secure Communication on Nostr.";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
Onboarding::new(window, cx)
@@ -97,7 +97,7 @@ impl Render for Onboarding {
.flex_col()
.items_center()
.justify_center()
.gap_8()
.gap_10()
.child(
div()
.flex()
@@ -107,7 +107,7 @@ impl Render for Onboarding {
.child(
svg()
.path(LOGO_URL)
.size_12()
.size_16()
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
)
.child(
@@ -115,14 +115,13 @@ impl Render for Onboarding {
.text_center()
.child(
div()
.text_lg()
.text_xl()
.font_semibold()
.line_height(relative(1.2))
.line_height(relative(1.3))
.child(TITLE),
)
.child(
div()
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(SUBTITLE),
),

View File

@@ -2,46 +2,38 @@ use async_utility::task::spawn;
use common::nip96_upload;
use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render,
SharedString, Styled, Task, Window,
div, img, prelude::FluentBuilder, px, App, AppContext, Context, Entity, Flatten, IntoElement,
ParentElement, PathPromptOptions, Render, Styled, Task, Window,
};
use nostr_sdk::prelude::*;
use smol::fs;
use std::{str::FromStr, sync::Arc, time::Duration};
use std::{str::FromStr, time::Duration};
use ui::{
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
input::TextInput,
popup_menu::PopupMenu,
ContextModal, Disableable, Sizable, Size,
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, IconName, Sizable, Size,
};
pub fn init(window: &mut Window, cx: &mut App) -> Arc<Entity<Profile>> {
Arc::new(Profile::new(window, cx))
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Profile> {
Profile::new(window, cx)
}
pub struct Profile {
profile: Option<Metadata>,
// Form
name_input: Entity<TextInput>,
avatar_input: Entity<TextInput>,
bio_input: Entity<TextInput>,
website_input: Entity<TextInput>,
is_loading: bool,
is_submitting: bool,
// Panel
name: SharedString,
focus_handle: FocusHandle,
}
impl Profile {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let window_handle = window.window_handle();
let name_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::XSmall)
.text_size(Size::Small)
.placeholder("Alice")
});
@@ -54,13 +46,13 @@ impl Profile {
let website_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::XSmall)
.text_size(Size::Small)
.placeholder("https://your-website.com")
});
let bio_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::XSmall)
.text_size(Size::Small)
.multi_line()
.placeholder("A short introduce about you.")
});
@@ -74,8 +66,6 @@ impl Profile {
profile: None,
is_loading: false,
is_submitting: false,
name: "Profile".into(),
focus_handle: cx.focus_handle(),
};
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
@@ -89,10 +79,10 @@ impl Profile {
Ok(metadata)
});
cx.spawn(async move |this, cx| {
cx.spawn_in(window, async move |this, cx| {
if let Ok(Some(metadata)) = task.await {
_ = cx.update_window(window_handle, |_, window, cx| {
_ = this.update(cx, |this: &mut Profile, cx| {
cx.update(|window, cx| {
this.update(cx, |this: &mut Profile, cx| {
this.avatar_input.update(cx, |this, cx| {
if let Some(avatar) = metadata.picture.as_ref() {
this.set_text(avatar, window, cx);
@@ -114,9 +104,12 @@ impl Profile {
}
});
this.profile = Some(metadata);
cx.notify();
});
});
})
.ok();
})
.ok();
}
})
.detach();
@@ -127,7 +120,6 @@ impl Profile {
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let avatar_input = self.avatar_input.downgrade();
let window_handle = window.window_handle();
let paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: false,
@@ -137,7 +129,7 @@ impl Profile {
// Show loading spinner
self.set_loading(true, cx);
cx.spawn(async move |this, cx| {
cx.spawn_in(window, async move |this, cx| {
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
Ok(Some(mut paths)) => {
let path = paths.pop().unwrap();
@@ -153,32 +145,33 @@ impl Profile {
});
if let Ok(url) = rx.await {
cx.update_window(window_handle, |_, window, cx| {
cx.update(|window, cx| {
// Stop loading spinner
this.update(cx, |this, cx| {
this.set_loading(false, cx);
})
.unwrap();
.ok();
// Set avatar input
avatar_input
.update(cx, |this, cx| {
this.set_text(url.to_string(), window, cx);
})
.unwrap();
.ok();
})
.unwrap();
.ok();
}
}
}
Ok(None) => {
// Stop loading spinner
if let Some(view) = this.upgrade() {
cx.update_entity(&view, |this, cx| {
cx.update(|_, cx| {
// Stop loading spinner
this.update(cx, |this, cx| {
this.set_loading(false, cx);
})
.unwrap();
}
.ok();
})
.ok();
}
Err(_) => {}
}
@@ -186,11 +179,6 @@ impl Profile {
.detach();
}
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
self.is_loading = status;
cx.notify();
}
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Show loading spinner
self.set_submitting(true, cx);
@@ -216,82 +204,57 @@ impl Profile {
new_metadata = new_metadata.website(url);
}
let window_handle = window.window_handle();
cx.spawn(async move |this, cx| {
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = get_client();
let (tx, rx) = oneshot::channel::<EventId>();
_ = client.set_metadata(&new_metadata).await?;
cx.background_spawn(async move {
if let Ok(output) = client.set_metadata(&new_metadata).await {
_ = tx.send(output.val);
}
})
.detach();
Ok(())
});
if rx.await.is_ok() {
cx.update_window(window_handle, |_, window, cx| {
cx.spawn_in(window, async move |this, cx| {
if task.await.is_ok() {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.set_submitting(false, cx);
window.push_notification("Your profile has been updated successfully", cx);
})
.unwrap()
.ok();
})
.unwrap();
.ok();
}
})
.detach();
}
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
self.is_loading = status;
cx.notify();
}
fn set_submitting(&mut self, status: bool, cx: &mut Context<Self>) {
self.is_submitting = status;
cx.notify();
}
}
impl Panel for Profile {
fn panel_id(&self) -> SharedString {
"ProfilePanel".into()
}
fn title(&self, _cx: &App) -> AnyElement {
self.name.clone().into_any_element()
}
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
menu.track_focus(&self.focus_handle)
}
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
vec![]
}
}
impl EventEmitter<PanelEvent> for Profile {}
impl Focusable for Profile {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Profile {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.px_2()
.flex()
.flex_col()
.gap_3()
.child(
div()
.w_full()
.h_32()
.bg(cx.theme().base.step(cx, ColorScaleStep::TWO))
.rounded(px(cx.theme().radius))
.flex()
.flex_col()
.items_center()
.justify_end()
.justify_center()
.gap_2()
.w_full()
.h_24()
.map(|this| {
let picture = self.avatar_input.read(cx).text();
@@ -310,23 +273,16 @@ impl Render for Profile {
}
})
.child(
div()
.flex()
.gap_1()
.items_center()
.w_full()
.child(self.avatar_input.clone())
.child(
Button::new("upload")
.label("Upload")
.ghost()
.small()
.disabled(self.is_submitting)
.loading(self.is_loading)
.on_click(cx.listener(move |this, _, window, cx| {
this.upload(window, cx);
})),
),
Button::new("upload")
.icon(IconName::Upload)
.label("Change")
.ghost()
.small()
.disabled(self.is_loading || self.is_submitting)
.loading(self.is_loading)
.on_click(cx.listener(move |this, _, window, cx| {
this.upload(window, cx);
})),
),
)
.child(
@@ -334,7 +290,7 @@ impl Render for Profile {
.flex()
.flex_col()
.gap_1()
.text_xs()
.text_sm()
.child("Name:")
.child(self.name_input.clone()),
)
@@ -343,26 +299,25 @@ impl Render for Profile {
.flex()
.flex_col()
.gap_1()
.text_xs()
.child("Bio:")
.child(self.bio_input.clone()),
.text_sm()
.child("Website:")
.child(self.website_input.clone()),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.text_xs()
.child("Website:")
.child(self.website_input.clone()),
.text_sm()
.child("Bio:")
.child(self.bio_input.clone()),
)
.child(
div().flex().items_center().justify_end().child(
div().mt_2().w_full().child(
Button::new("submit")
.label("Update")
.primary()
.small()
.disabled(self.is_loading)
.disabled(self.is_loading || self.is_submitting)
.loading(self.is_submitting)
.on_click(cx.listener(move |this, _, window, cx| {
this.submit(window, cx);

View File

@@ -3,7 +3,7 @@ use global::{constants::NEW_MESSAGE_SUB_ID, get_client};
use gpui::{
div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Context, Entity, FocusHandle,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign,
Window,
UniformList, Window,
};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
@@ -11,10 +11,11 @@ use ui::{
button::{Button, ButtonVariants},
input::{InputEvent, TextInput},
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, IconName, Sizable,
ContextModal, Disableable, IconName, Sizable,
};
const MESSAGE: &str = "In order to receive messages from others, you need to setup Messaging Relays. You can use the recommend relays or add more.";
const MIN_HEIGHT: f32 = 200.0;
const MESSAGE: &str = "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more.";
const HELP_TEXT: &str = "Please add some relays.";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Relays> {
@@ -32,7 +33,12 @@ pub struct Relays {
impl Relays {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let client = get_client();
let input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(ui::Size::XSmall)
.small()
.placeholder("wss://example.com")
});
let relays = cx.new(|cx| {
let relays = vec![
@@ -41,6 +47,7 @@ impl Relays {
];
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
@@ -82,13 +89,6 @@ impl Relays {
relays
});
let input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(ui::Size::XSmall)
.small()
.placeholder("wss://example.com")
});
cx.new(|cx| {
let mut subscriptions = smallvec![];
@@ -113,11 +113,9 @@ impl Relays {
}
pub fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Show loading spinner
self.set_loading(true, cx);
let relays = self.relays.read(cx).clone();
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
@@ -189,10 +187,6 @@ impl Relays {
.detach();
}
pub fn loading(&self) -> bool {
self.is_loading
}
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
self.is_loading = status;
cx.notify();
@@ -225,116 +219,136 @@ impl Relays {
cx.notify();
});
}
fn render_list(
&mut self,
relays: Vec<RelayUrl>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> UniformList {
let view = cx.entity();
let total = relays.len();
uniform_list(view, "relays", total, move |_, range, _window, cx| {
let mut items = Vec::new();
for ix in range {
let item = relays.get(ix).unwrap().clone().to_string();
items.push(
div().group("").w_full().h_9().py_0p5().child(
div()
.px_2()
.h_full()
.w_full()
.flex()
.items_center()
.justify_between()
.rounded(px(cx.theme().radius))
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
.text_xs()
.child(item)
.child(
Button::new("remove_{ix}")
.icon(IconName::Close)
.xsmall()
.ghost()
.invisible()
.group_hover("", |this| this.visible())
.on_click(cx.listener(move |this, _, window, cx| {
this.remove(ix, window, cx)
})),
),
),
)
}
items
})
.w_full()
.min_h(px(MIN_HEIGHT))
}
fn render_empty(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.h_20()
.mb_2()
.flex()
.items_center()
.justify_center()
.text_sm()
.text_align(TextAlign::Center)
.child(HELP_TEXT)
}
}
impl Render for Relays {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.w_full()
.h_full()
.flex()
.flex_col()
.gap_2()
.w_full()
.justify_between()
.child(
div()
.px_2()
.text_xs()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(MESSAGE),
)
.child(
div()
.px_2()
.w_full()
.flex_1()
.flex()
.flex_col()
.gap_2()
.child(
div()
.flex()
.items_center()
.w_full()
.gap_2()
.child(self.input.clone())
.child(
Button::new("add_relay_btn")
.icon(IconName::Plus)
.small()
.rounded(px(cx.theme().radius))
.on_click(
cx.listener(|this, _, window, cx| this.add(window, cx)),
),
),
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(MESSAGE),
)
.map(|this| {
let view = cx.entity();
let relays = self.relays.read(cx).clone();
let total = relays.len();
if !relays.is_empty() {
this.child(
uniform_list(
view,
"relays",
total,
move |_, range, _window, cx| {
let mut items = Vec::new();
for ix in range {
let item = relays.get(ix).unwrap().clone().to_string();
items.push(
div().group("").w_full().h_9().py_0p5().child(
div()
.px_2()
.h_full()
.w_full()
.flex()
.items_center()
.justify_between()
.rounded(px(cx.theme().radius))
.bg(cx
.theme()
.base
.step(cx, ColorScaleStep::THREE))
.text_xs()
.child(item)
.child(
Button::new("remove_{ix}")
.icon(IconName::Close)
.xsmall()
.ghost()
.invisible()
.group_hover("", |this| {
this.visible()
})
.on_click(cx.listener(
move |this, _, window, cx| {
this.remove(ix, window, cx)
},
)),
),
),
)
}
items
},
)
.w_full()
.min_h(px(120.)),
.child(
div()
.w_full()
.flex()
.flex_col()
.gap_3()
.child(
div()
.flex()
.items_center()
.w_full()
.gap_2()
.child(self.input.clone())
.child(
Button::new("add_relay_btn")
.icon(IconName::Plus)
.label("Add")
.small()
.ghost()
.rounded(px(cx.theme().radius))
.on_click(cx.listener(|this, _, window, cx| {
this.add(window, cx)
})),
),
)
} else {
this.h_20()
.mb_2()
.flex()
.items_center()
.justify_center()
.text_xs()
.text_align(TextAlign::Center)
.child(HELP_TEXT)
}
}),
.map(|this| {
let relays = self.relays.read(cx).clone();
if !relays.is_empty() {
this.child(self.render_list(relays, window, cx))
} else {
this.child(self.render_empty(window, cx))
}
}),
),
)
.child(
Button::new("submti")
.label("Update")
.primary()
.w_full()
.loading(self.is_loading)
.disabled(self.is_loading)
.on_click(cx.listener(|this, _, window, cx| {
this.update(window, cx);
})),
)
}
}

View File

@@ -1,76 +0,0 @@
use gpui::{
div, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use ui::{
button::Button,
dock_area::panel::{Panel, PanelEvent},
popup_menu::PopupMenu,
};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Settings> {
Settings::new(window, cx)
}
pub struct Settings {
name: SharedString,
closable: bool,
zoomable: bool,
focus_handle: FocusHandle,
}
impl Settings {
pub fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
name: "Settings".into(),
closable: true,
zoomable: true,
focus_handle: cx.focus_handle(),
})
}
}
impl Panel for Settings {
fn panel_id(&self) -> SharedString {
"SettingsPanel".into()
}
fn title(&self, _cx: &App) -> AnyElement {
self.name.clone().into_any_element()
}
fn closable(&self, _cx: &App) -> bool {
self.closable
}
fn zoomable(&self, _cx: &App) -> bool {
self.zoomable
}
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
menu.track_focus(&self.focus_handle)
}
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
vec![]
}
}
impl EventEmitter<PanelEvent> for Settings {}
impl Focusable for Settings {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Settings {
fn render(&mut self, _window: &mut gpui::Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.child("Settings")
}
}

View File

@@ -0,0 +1,58 @@
use std::rc::Rc;
use gpui::{
div, prelude::FluentBuilder, px, App, ClickEvent, Div, InteractiveElement, IntoElement,
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
};
use ui::{
theme::{scale::ColorScaleStep, ActiveTheme},
Icon,
};
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
#[derive(IntoElement)]
pub struct SidebarButton {
base: Div,
label: SharedString,
icon: Option<Icon>,
handler: Handler,
}
impl SidebarButton {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
base: div().flex().items_center().gap_3().px_3().h_8(),
label: label.into(),
icon: None,
handler: Rc::new(|_, _, _| {}),
}
}
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.handler = Rc::new(handler);
self
}
}
impl RenderOnce for SidebarButton {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let handler = self.handler.clone();
self.base
.id(self.label.clone())
.rounded(px(cx.theme().radius))
.when_some(self.icon, |this, icon| this.child(icon))
.child(self.label.clone())
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.on_click(move |ev, window, cx| handler(ev, window, cx))
}
}

View File

@@ -1,20 +1,21 @@
use std::rc::Rc;
use gpui::{
div, prelude::FluentBuilder, px, App, ClickEvent, Img, InteractiveElement, IntoElement,
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled as _, Window,
div, percentage, prelude::FluentBuilder, px, App, ClickEvent, Div, Img, InteractiveElement,
IntoElement, ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled,
Window,
};
use ui::{
theme::{scale::ColorScaleStep, ActiveTheme},
Collapsible, Icon, IconName, StyledExt,
Collapsible, Icon, IconName, Sizable, StyledExt,
};
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
#[derive(IntoElement)]
pub struct Parent {
base: Div,
icon: Option<Icon>,
active_icon: Option<Icon>,
label: SharedString,
items: Vec<Folder>,
collapsed: bool,
@@ -24,9 +25,9 @@ pub struct Parent {
impl Parent {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
base: div().flex().flex_col().gap_2(),
label: label.into(),
icon: None,
active_icon: None,
items: Vec::new(),
collapsed: false,
handler: Rc::new(|_, _, _| {}),
@@ -38,11 +39,6 @@ impl Parent {
self
}
pub fn active_icon(mut self, icon: impl Into<Icon>) -> Self {
self.active_icon = Some(icon.into());
self
}
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
self
@@ -83,10 +79,7 @@ impl RenderOnce for Parent {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let handler = self.handler.clone();
div()
.flex()
.flex_col()
.gap_1()
self.base
.child(
div()
.id(self.label.clone())
@@ -94,36 +87,37 @@ impl RenderOnce for Parent {
.items_center()
.gap_2()
.px_2()
.h_6()
.h_8()
.rounded(px(cx.theme().radius))
.text_xs()
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.font_semibold()
.when_some(self.icon, |this, icon| {
this.map(|this| {
if self.collapsed {
this.child(icon.size_4())
} else {
this.when_some(self.active_icon, |this, icon| {
this.child(icon.size_4())
})
}
})
})
.child(self.label.clone())
.font_medium()
.child(
Icon::new(IconName::CaretDown)
.xsmall()
.when(self.collapsed, |this| this.rotate(percentage(270. / 360.))),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.when_some(self.icon, |this, icon| this.child(icon.small()))
.child(self.label.clone()),
)
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.on_click(move |ev, window, cx| handler(ev, window, cx)),
)
.when(!self.collapsed, |this| {
this.child(div().flex().flex_col().gap_1().pl_3().children(self.items))
this.child(div().flex().flex_col().gap_2().pl_3().children(self.items))
})
}
}
#[derive(IntoElement)]
pub struct Folder {
base: Div,
icon: Option<Icon>,
active_icon: Option<Icon>,
label: SharedString,
items: Vec<FolderItem>,
collapsed: bool,
@@ -133,9 +127,9 @@ pub struct Folder {
impl Folder {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
base: div().flex().flex_col().gap_2(),
label: label.into(),
icon: None,
active_icon: None,
items: Vec::new(),
collapsed: false,
handler: Rc::new(|_, _, _| {}),
@@ -147,11 +141,6 @@ impl Folder {
self
}
pub fn active_icon(mut self, icon: impl Into<Icon>) -> Self {
self.active_icon = Some(icon.into());
self
}
pub fn collapsed(mut self, collapsed: bool) -> Self {
self.collapsed = collapsed;
self
@@ -186,10 +175,7 @@ impl RenderOnce for Folder {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let handler = self.handler.clone();
div()
.flex()
.flex_col()
.gap_1()
self.base
.child(
div()
.id(self.label.clone())
@@ -197,23 +183,24 @@ impl RenderOnce for Folder {
.items_center()
.gap_2()
.px_2()
.h_6()
.h_8()
.rounded(px(cx.theme().radius))
.text_xs()
.text_sm()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.font_semibold()
.when_some(self.icon, |this, icon| {
this.map(|this| {
if self.collapsed {
this.child(icon.size_4())
} else {
this.when_some(self.active_icon, |this, icon| {
this.child(icon.size_4())
})
}
})
})
.child(self.label.clone())
.font_medium()
.child(
Icon::new(IconName::CaretDown)
.xsmall()
.when(self.collapsed, |this| this.rotate(percentage(270. / 360.))),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.when_some(self.icon, |this, icon| this.child(icon.small()))
.child(self.label.clone()),
)
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.on_click(move |ev, window, cx| handler(ev, window, cx)),
)
@@ -226,6 +213,7 @@ impl RenderOnce for Folder {
#[derive(IntoElement)]
pub struct FolderItem {
ix: usize,
base: Div,
img: Option<Img>,
label: Option<SharedString>,
description: Option<SharedString>,
@@ -236,6 +224,7 @@ impl FolderItem {
pub fn new(ix: usize) -> Self {
Self {
ix,
base: div().h_8().w_full().px_2(),
img: None,
label: None,
description: None,
@@ -271,15 +260,12 @@ impl RenderOnce for FolderItem {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let handler = self.handler.clone();
div()
self.base
.id(self.ix)
.h_6()
.px_2()
.w_full()
.flex()
.items_center()
.justify_between()
.text_xs()
.text_sm()
.rounded(px(cx.theme().radius))
.child(
div()
@@ -291,19 +277,21 @@ impl RenderOnce for FolderItem {
.font_medium()
.map(|this| {
if let Some(img) = self.img {
this.child(img.size_4().flex_shrink_0())
this.child(img.size_5().flex_shrink_0())
} else {
this.child(
div()
.flex()
.justify_center()
.items_center()
.size_4()
.size_5()
.rounded_full()
.bg(cx.theme().accent.step(cx, ColorScaleStep::THREE))
.child(Icon::new(IconName::GroupFill).size_2().text_color(
cx.theme().accent.step(cx, ColorScaleStep::TWELVE),
)),
.child(
Icon::new(IconName::UsersThreeFill).xsmall().text_color(
cx.theme().accent.step(cx, ColorScaleStep::TWELVE),
),
),
)
}
})
@@ -313,11 +301,12 @@ impl RenderOnce for FolderItem {
this.child(
div()
.flex_shrink_0()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.text_xs()
.text_color(cx.theme().base.step(cx, ColorScaleStep::TEN))
.child(description),
)
})
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)))
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
.on_click(move |ev, window, cx| handler(ev, window, cx))
}
}

View File

@@ -1,29 +1,37 @@
use account::Account;
use button::SidebarButton;
use chats::{
room::{Room, RoomKind},
ChatRegistry,
};
use compose::{Compose, ComposeButton};
use common::profile::SharedProfile;
use folder::{Folder, FolderItem, Parent};
use global::get_client;
use gpui::{
div, img, prelude::FluentBuilder, px, AnyElement, App, AppContext, Context, Entity,
EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled,
Window,
actions, div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity,
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
SharedString, Styled, Task, Window,
};
use ui::{
button::{Button, ButtonRounded, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
popup_menu::PopupMenu,
button::{Button, ButtonVariants},
dock_area::{
dock::DockPlacement,
panel::{Panel, PanelEvent},
},
popup_menu::{PopupMenu, PopupMenuExt},
scroll::ScrollbarAxis,
skeleton::Skeleton,
theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, IconName, StyledExt,
IconName, Sizable, StyledExt,
};
use crate::chat_space::{AddPanel, PanelKind};
use crate::chat_space::{AddPanel, ModalKind, PanelKind, ToggleModal};
mod compose;
mod button;
mod folder;
actions!(profile, [Logout]);
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
Sidebar::new(window, cx)
}
@@ -55,49 +63,6 @@ impl Sidebar {
}
}
fn render_compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let compose = cx.new(|cx| Compose::new(window, cx));
window.open_modal(cx, move |modal, window, cx| {
let label = compose.read(cx).label(window, cx);
let is_submitting = compose.read(cx).is_submitting();
modal
.title("Direct Messages")
.width(px(420.))
.child(compose.clone())
.footer(
div()
.p_2()
.border_t_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
.child(
Button::new("create_dm_btn")
.label(label)
.primary()
.bold()
.rounded(ButtonRounded::Large)
.w_full()
.loading(is_submitting)
.disabled(is_submitting)
.on_click(window.listener_for(&compose, |this, _, window, cx| {
this.compose(window, cx)
})),
),
)
})
}
fn open_room(&self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
window.dispatch_action(
Box::new(AddPanel::new(
PanelKind::Room(id),
ui::dock_area::dock::DockPlacement::Center,
)),
cx,
);
}
fn ongoing(&mut self, cx: &mut Context<Self>) {
self.ongoing = !self.ongoing;
cx.notify();
@@ -118,7 +83,6 @@ impl Sidebar {
cx.notify();
}
#[allow(dead_code)]
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
(0..total).map(|_| {
div()
@@ -148,8 +112,11 @@ impl Sidebar {
.description(ago)
.img(img)
.on_click({
cx.listener(move |this, _, window, cx| {
this.open_room(id, window, cx);
cx.listener(move |_, _, window, cx| {
window.dispatch_action(
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
cx,
);
})
});
@@ -158,6 +125,28 @@ impl Sidebar {
items
}
fn on_logout(&mut self, _: &Logout, window: &mut Window, cx: &mut Context<Self>) {
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
let client = get_client();
_ = client.reset().await;
Ok(())
});
cx.spawn_in(window, async move |_, cx| {
if task.await.is_ok() {
cx.update(|_, cx| {
Account::global(cx).update(cx, |this, cx| {
this.profile = None;
cx.notify();
});
})
.ok();
};
})
.detach();
}
}
impl Panel for Sidebar {
@@ -188,7 +177,9 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let account = Account::global(cx).read(cx).profile.as_ref();
let registry = ChatRegistry::global(cx).read(cx);
let rooms = registry.rooms(cx);
let loading = registry.loading();
@@ -198,67 +189,149 @@ impl Render for Sidebar {
div()
.scrollable(cx.entity_id(), ScrollbarAxis::Vertical)
.on_action(cx.listener(Self::on_logout))
.size_full()
.flex()
.flex_col()
.gap_3()
.pt_1()
.px_2()
.py_3()
.child(ComposeButton::new("New Message").on_click(cx.listener(
|this, _, window, cx| {
this.render_compose(window, cx);
},
)))
.map(|this| {
if loading {
this.children(self.render_skeleton(6))
} else {
this.when_some(ongoing, |this, rooms| {
this.child(
Folder::new("Ongoing")
.icon(IconName::FolderFill)
.active_icon(IconName::FolderOpenFill)
.collapsed(self.ongoing)
.on_click(cx.listener(move |this, _, _, cx| {
this.ongoing(cx);
}))
.children(Self::render_items(rooms, cx)),
.pb_2()
.when_some(account, |this, profile| {
this.child(
div()
.h_7()
.px_1p5()
.flex()
.justify_between()
.items_center()
.child(
div()
.flex()
.items_center()
.gap_2()
.text_sm()
.font_semibold()
.child(img(profile.shared_avatar()).size_7())
.child(profile.shared_name()),
)
})
.child(
Button::new("user_dropdown")
.icon(IconName::Ellipsis)
.small()
.ghost()
.popup_menu(|this, _window, _cx| {
this.menu(
"Profile",
Box::new(ToggleModal {
modal: ModalKind::Profile,
}),
)
.menu(
"Relays",
Box::new(ToggleModal {
modal: ModalKind::Relay,
}),
)
.separator()
.menu("Logout", Box::new(Logout))
}),
),
)
})
.child(
div()
.flex()
.flex_col()
.gap_1()
.text_sm()
.font_medium()
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(
Parent::new("Incoming")
.icon(IconName::FolderFill)
.active_icon(IconName::FolderOpenFill)
.collapsed(self.incoming)
.on_click(cx.listener(move |this, _, _, cx| {
this.incoming(cx);
}))
.when_some(trusted, |this, rooms| {
SidebarButton::new("New Message")
.icon(IconName::PlusCircleFill)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(
Box::new(ToggleModal {
modal: ModalKind::Compose,
}),
cx,
);
})),
)
.child(
SidebarButton::new("Contacts")
.icon(IconName::AddressBook)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(
Box::new(ToggleModal {
modal: ModalKind::Contact,
}),
cx,
);
})),
),
)
.child(
div()
.flex()
.flex_col()
.gap_2()
.child(
div()
.px_2()
.text_xs()
.font_semibold()
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
.child("Messages"),
)
.map(|this| {
if loading {
this.children(self.render_skeleton(6))
} else {
this.when_some(ongoing, |this, rooms| {
this.child(
Folder::new("Trusted")
.icon(IconName::FolderFill)
.active_icon(IconName::FolderOpenFill)
.collapsed(self.trusted)
Folder::new("Ongoing")
.icon(IconName::Folder)
.collapsed(self.ongoing)
.on_click(cx.listener(move |this, _, _, cx| {
this.trusted(cx);
this.ongoing(cx);
}))
.children(Self::render_items(rooms, cx)),
)
})
.when_some(unknown, |this, rooms| {
this.child(
Folder::new("Unknown")
.icon(IconName::FolderFill)
.active_icon(IconName::FolderOpenFill)
.collapsed(self.unknown)
.on_click(cx.listener(move |this, _, _, cx| {
this.unknown(cx);
}))
.children(Self::render_items(rooms, cx)),
)
}),
)
}
})
.child(
Parent::new("Incoming")
.icon(IconName::Folder)
.collapsed(self.incoming)
.on_click(cx.listener(move |this, _, _, cx| {
this.incoming(cx);
}))
.when_some(trusted, |this, rooms| {
this.child(
Folder::new("Trusted")
.icon(IconName::Folder)
.collapsed(self.trusted)
.on_click(cx.listener(move |this, _, _, cx| {
this.trusted(cx);
}))
.children(Self::render_items(rooms, cx)),
)
})
.when_some(unknown, |this, rooms| {
this.child(
Folder::new("Unknown")
.icon(IconName::Folder)
.collapsed(self.unknown)
.on_click(cx.listener(move |this, _, _, cx| {
this.unknown(cx);
}))
.children(Self::render_items(rooms, cx)),
)
}),
)
}
}),
)
}
}

View File

@@ -42,7 +42,7 @@ impl Panel for Welcome {
}
fn title(&self, _cx: &App) -> AnyElement {
self.name.clone().into_any_element()
"👋".into_any_element()
}
fn closable(&self, _cx: &App) -> bool {
@@ -92,8 +92,8 @@ impl Render for Welcome {
.child(
div()
.child("coop on nostr.")
.text_color(cx.theme().base.step(cx, ColorScaleStep::FOUR))
.font_black()
.text_color(cx.theme().base.step(cx, ColorScaleStep::NINE))
.font_semibold()
.text_sm(),
),
)

View File

@@ -357,7 +357,7 @@ impl RenderOnce for Button {
Size::XSmall => this.h_6().px_0p5(),
Size::Small => this.h_7().px_2(),
Size::Large => this.h_10().px_3(),
_ => this.h_8().px_3(),
_ => this.h_9().px_2(),
}
}
})
@@ -437,7 +437,7 @@ impl RenderOnce for Button {
.id("label")
.items_center()
.justify_center()
.text_xs()
.text_sm()
.map(|this| match self.size {
Size::XSmall => this.gap_0p5(),
Size::Small => this.gap_1(),
@@ -451,7 +451,6 @@ impl RenderOnce for Button {
.when(self.loading, |this| {
this.child(
Indicator::new()
.with_size(self.size)
.when_some(self.loading_icon, |this, icon| this.icon(icon)),
)
})

View File

@@ -407,19 +407,19 @@ impl TabPanel {
let build_popup_menu = move |this, cx: &App| view.read(cx).popup_menu(this, cx);
h_flex()
.gap_2()
.gap_1()
.occlude()
.items_center()
.children(
self.toolbar_buttons(window, cx)
.into_iter()
.map(|btn| btn.xsmall().ghost()),
.map(|btn| btn.small().ghost()),
)
.when(self.is_zoomed, |this| {
this.child(
Button::new("zoom")
.icon(IconName::Minimize)
.xsmall()
.icon(IconName::ArrowIn)
.small()
.ghost()
.tooltip("Zoom Out")
.on_click(cx.listener(|view, _, window, cx| {
@@ -430,7 +430,7 @@ impl TabPanel {
.child(
Button::new("menu")
.icon(IconName::Ellipsis)
.xsmall()
.small()
.ghost()
.popup_menu({
let zoomable = state.zoomable;
@@ -451,7 +451,7 @@ impl TabPanel {
)
}
fn render_dock_toggle_button(
fn _render_dock_toggle_button(
&self,
placement: DockPlacement,
_window: &mut Window,
@@ -517,7 +517,7 @@ impl TabPanel {
Some(
Button::new(SharedString::from(format!("toggle-dock:{:?}", placement)))
.icon(icon)
.xsmall()
.small()
.ghost()
.tooltip(match is_open {
true => "Collapse",
@@ -547,9 +547,6 @@ impl TabPanel {
};
let panel_style = dock_area.read(cx).panel_style;
let left_dock_button = self.render_dock_toggle_button(DockPlacement::Left, window, cx);
let bottom_dock_button = self.render_dock_toggle_button(DockPlacement::Bottom, window, cx);
let right_dock_button = self.render_dock_toggle_button(DockPlacement::Right, window, cx);
if self.panels.len() == 1 && panel_style == PanelStyle::Default {
let panel = self.panels.first().unwrap();
@@ -565,21 +562,6 @@ impl TabPanel {
.h(px(30.))
.py_2()
.px_3()
.when(left_dock_button.is_some(), |this| this.pl_2())
.when(right_dock_button.is_some(), |this| this.pr_2())
.when(
left_dock_button.is_some() || bottom_dock_button.is_some(),
|this| {
this.child(
h_flex()
.flex_shrink_0()
.mr_1()
.gap_1()
.children(left_dock_button)
.children(bottom_dock_button),
)
},
)
.child(
div()
.id("tab")
@@ -612,8 +594,7 @@ impl TabPanel {
.flex_shrink_0()
.ml_1()
.gap_1()
.child(self.render_toolbar(state, window, cx))
.children(right_dock_button),
.child(self.render_toolbar(state, window, cx)),
)
.into_any_element();
}
@@ -622,22 +603,6 @@ impl TabPanel {
TabBar::new("tab-bar")
.track_scroll(self.tab_bar_scroll_handle.clone())
.when(
left_dock_button.is_some() || bottom_dock_button.is_some(),
|this| {
this.prefix(
h_flex()
.items_center()
.top_0()
// Right -1 for avoid border overlap with the first tab
.right(-px(1.))
.h_full()
.px_2()
.children(left_dock_button)
.children(bottom_dock_button),
)
},
)
.children(self.panels.iter().enumerate().filter_map(|(ix, panel)| {
let mut active = state.active_panel.as_ref() == Some(panel);
let disabled = self.is_collapsed;
@@ -723,8 +688,7 @@ impl TabPanel {
.h_full()
.px_2()
.gap_1()
.child(self.render_toolbar(state, window, cx))
.when_some(right_dock_button, |this, btn| this.child(btn)),
.child(self.render_toolbar(state, window, cx)),
)
.into_any_element()
}

View File

@@ -661,9 +661,9 @@ where
Some(icon) => icon,
None => {
if self.open {
IconName::ChevronUp
IconName::CaretUp
} else {
IconName::ChevronDown
IconName::CaretDown
}
}
};

View File

@@ -10,61 +10,37 @@ use gpui::{
#[derive(IntoElement, Clone)]
pub enum IconName {
ALargeSmall,
AddressBook,
ArrowIn,
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
ArrowUpCircle,
Asterisk,
Bell,
BookOpen,
Bot,
BubbleFill,
Calendar,
ChartPie,
CaretUp,
CaretDown,
CaretDownFill,
CaretRight,
Check,
ChevronDown,
ChevronDownSmall,
ChevronLeft,
ChevronRight,
ChevronUp,
ChevronsUpDown,
CircleCheck,
CircleUser,
CircleX,
CheckCircle,
CheckCircleFill,
Close,
CloseCircle,
CloseCircleFill,
Copy,
ComposeFill,
Dash,
Delete,
Ellipsis,
EllipsisVertical,
Eye,
EyeOff,
Frame,
Folder,
FolderFill,
FolderOpenFill,
GalleryVerticalEnd,
GitHub,
Globe,
Group,
GroupFill,
Heart,
HeartOff,
Inbox,
Info,
LayoutDashboard,
Loader,
LoaderCircle,
Map,
Maximize,
MailboxFill,
Menu,
Minimize,
Minus,
Moon,
Relays,
Palette,
PanelBottom,
PanelBottomOpen,
@@ -75,20 +51,19 @@ pub enum IconName {
PanelRightClose,
PanelRightOpen,
Plus,
PlusCircleFill,
Relays,
ResizeCorner,
Search,
Settings,
Settings2,
SortAscending,
SortDescending,
SquareTerminal,
Star,
StarOff,
Sun,
ThumbsDown,
ThumbsUp,
TriangleAlert,
Upload,
ResizeCorner,
UsersThreeFill,
WindowClose,
WindowMaximize,
WindowMinimize,
@@ -98,61 +73,37 @@ pub enum IconName {
impl IconName {
pub fn path(self) -> SharedString {
match self {
Self::ALargeSmall => "icons/a-large-small.svg",
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::Asterisk => "icons/asterisk.svg",
Self::Bell => "icons/bell.svg",
Self::BookOpen => "icons/book-open.svg",
Self::Bot => "icons/bot.svg",
Self::BubbleFill => "icons/bubble-fill.svg",
Self::Calendar => "icons/calendar.svg",
Self::ChartPie => "icons/chart-pie.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::ChevronDown => "icons/chevron-down.svg",
Self::ChevronDownSmall => "icons/chevron-down-small.svg",
Self::ChevronLeft => "icons/chevron-left.svg",
Self::ChevronRight => "icons/chevron-right.svg",
Self::ChevronUp => "icons/chevron-up.svg",
Self::ChevronsUpDown => "icons/chevrons-up-down.svg",
Self::CircleCheck => "icons/circle-check.svg",
Self::CircleUser => "icons/circle-user.svg",
Self::CircleX => "icons/circle-x.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::ComposeFill => "icons/compose-fill.svg",
Self::Dash => "icons/dash.svg",
Self::Delete => "icons/delete.svg",
Self::Ellipsis => "icons/ellipsis.svg",
Self::EllipsisVertical => "icons/ellipsis-vertical.svg",
Self::Eye => "icons/eye.svg",
Self::EyeOff => "icons/eye-off.svg",
Self::Frame => "icons/frame.svg",
Self::Folder => "icons/folder.svg",
Self::FolderFill => "icons/folder-fill.svg",
Self::FolderOpenFill => "icons/folder-open-fill.svg",
Self::GalleryVerticalEnd => "icons/gallery-vertical-end.svg",
Self::GitHub => "icons/github.svg",
Self::Globe => "icons/globe.svg",
Self::Group => "icons/group.svg",
Self::GroupFill => "icons/group-fill.svg",
Self::Heart => "icons/heart.svg",
Self::HeartOff => "icons/heart-off.svg",
Self::Inbox => "icons/inbox.svg",
Self::Info => "icons/info.svg",
Self::LayoutDashboard => "icons/layout-dashboard.svg",
Self::Loader => "icons/loader.svg",
Self::LoaderCircle => "icons/loader-circle.svg",
Self::Map => "icons/map.svg",
Self::Maximize => "icons/maximize.svg",
Self::MailboxFill => "icons/mailbox-fill.svg",
Self::Menu => "icons/menu.svg",
Self::Minimize => "icons/minimize.svg",
Self::Minus => "icons/minus.svg",
Self::Moon => "icons/moon.svg",
Self::Relays => "icons/relays.svg",
Self::Palette => "icons/palette.svg",
Self::PanelBottom => "icons/panel-bottom.svg",
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
@@ -163,20 +114,19 @@ impl IconName {
Self::PanelRightClose => "icons/panel-right-close.svg",
Self::PanelRightOpen => "icons/panel-right-open.svg",
Self::Plus => "icons/plus.svg",
Self::PlusCircleFill => "icons/plus-circle-fill.svg",
Self::Relays => "icons/relays.svg",
Self::ResizeCorner => "icons/resize-corner.svg",
Self::Search => "icons/search.svg",
Self::Settings => "icons/settings.svg",
Self::Settings2 => "icons/settings-2.svg",
Self::SortAscending => "icons/sort-ascending.svg",
Self::SortDescending => "icons/sort-descending.svg",
Self::SquareTerminal => "icons/square-terminal.svg",
Self::Star => "icons/star.svg",
Self::StarOff => "icons/star-off.svg",
Self::Sun => "icons/sun.svg",
Self::ThumbsDown => "icons/thumbs-down.svg",
Self::ThumbsUp => "icons/thumbs-up.svg",
Self::TriangleAlert => "icons/triangle-alert.svg",
Self::Upload => "icons/upload.svg",
Self::ResizeCorner => "icons/resize-corner.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",
@@ -311,8 +261,8 @@ impl RenderOnce for Icon {
.when_some(self.size, |this, size| match size {
Size::Size(px) => this.size(px),
Size::XSmall => this.size_3(),
Size::Small => this.size_3p5(),
Size::Medium => this.size_4(),
Size::Small => this.size_4(),
Size::Medium => this.size_5(),
Size::Large => this.size_6(),
})
.path(self.path)
@@ -341,8 +291,8 @@ impl Render for Icon {
.when_some(self.size, |this, size| match size {
Size::Size(px) => this.size(px),
Size::XSmall => this.size_3(),
Size::Small => this.size_3p5(),
Size::Medium => this.size_4(),
Size::Small => this.size_4(),
Size::Medium => this.size_5(),
Size::Large => this.size_6(),
})
.path(self.path.clone())

View File

@@ -16,7 +16,7 @@ pub struct Indicator {
impl Indicator {
pub fn new() -> Self {
Self {
size: Size::Medium,
size: Size::Small,
speed: Duration::from_secs_f64(0.8),
icon: Icon::new(IconName::Loader),
color: None,

View File

@@ -1568,6 +1568,7 @@ impl Render for TextInput {
let suffix = self.suffix.as_ref().map(|build| build(window, cx));
div()
.flex_1()
.flex()
.id("input")
.key_context(CONTEXT)

View File

@@ -1,14 +1,14 @@
use crate::{
animation::cubic_bezier,
button::{Button, ButtonVariants as _},
button::{Button, ButtonCustomVariant, ButtonVariants as _},
theme::{scale::ColorScaleStep, ActiveTheme as _},
v_flex, ContextModal, IconName, Sizable as _, StyledExt,
};
use gpui::{
actions, anchored, div, point, prelude::FluentBuilder, px, relative, Animation,
AnimationExt as _, AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, InteractiveElement,
IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString,
Styled, Window,
actions, anchored, div, point, prelude::FluentBuilder, px, Animation, AnimationExt as _,
AnyElement, App, Bounds, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement,
KeyBinding, MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, Styled,
Window,
};
use std::{rc::Rc, time::Duration};
@@ -45,9 +45,9 @@ impl Modal {
let base = v_flex()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE))
.rounded_lg()
.shadow_xl();
.border_color(cx.theme().base.step(cx, ColorScaleStep::SEVEN))
.rounded_xl()
.shadow_md();
Self {
base,
@@ -190,19 +190,48 @@ impl RenderOnce for Modal {
.top(y)
.w(self.width)
.when_some(self.max_width, |this, w| this.max_w(w))
.when_some(self.title, |this, title| {
this.child(
div()
.flex()
.items_center()
.h_10()
.px_2()
.text_sm()
.font_semibold()
.line_height(relative(1.))
.child(title),
)
})
.px_4()
.pb_4()
.child(
div()
.h_12()
.mb_2()
.border_b_1()
.border_color(cx.theme().base.step(cx, ColorScaleStep::SIX))
.flex()
.items_center()
.justify_between()
.when_some(self.title, |this, title| {
this.child(div().font_semibold().child(title))
})
.when(self.closable, |this| {
this.child(
Button::new(SharedString::from(format!(
"modal-close-{layer_ix}"
)))
.small()
.icon(IconName::CloseCircleFill)
.custom(
ButtonCustomVariant::new(window, cx)
.foreground(
cx.theme()
.base
.step(cx, ColorScaleStep::NINE),
)
.color(cx.theme().transparent)
.hover(cx.theme().transparent)
.active(cx.theme().transparent)
.border(cx.theme().transparent),
)
.on_click(move |_, window, cx| {
on_close(&ClickEvent::default(), window, cx);
window.close_modal(cx);
}),
)
}),
)
.child(self.content)
.children(self.footer)
.when(self.keyboard, |this| {
this.on_action({
let on_close = self.on_close.clone();
@@ -216,27 +245,6 @@ impl RenderOnce for Modal {
}
})
})
.when(self.closable, |this| {
this.child(
Button::new(SharedString::from(format!(
"modal-close-{layer_ix}"
)))
.absolute()
.top_2()
.right_2()
.small()
.ghost()
.icon(IconName::Close)
.on_click(
move |_, window, cx| {
on_close(&ClickEvent::default(), window, cx);
window.close_modal(cx);
},
),
)
})
.child(self.content)
.children(self.footer)
.with_animation(
"slide-down",
Animation::new(Duration::from_secs_f64(0.25))

View File

@@ -221,10 +221,9 @@ impl Render for Notification {
NotificationType::Info => {
Icon::new(IconName::Info).text_color(blue().step(cx, ColorScaleStep::NINE))
}
NotificationType::Error => {
Icon::new(IconName::CircleX).text_color(red().step(cx, ColorScaleStep::NINE))
}
NotificationType::Success => Icon::new(IconName::CircleCheck)
NotificationType::Error => Icon::new(IconName::CloseCircle)
.text_color(red().step(cx, ColorScaleStep::NINE)),
NotificationType::Success => Icon::new(IconName::CheckCircle)
.text_color(green().step(cx, ColorScaleStep::NINE)),
NotificationType::Warning => Icon::new(IconName::TriangleAlert)
.text_color(yellow().step(cx, ColorScaleStep::NINE)),

View File

@@ -574,7 +574,7 @@ impl Render for PopupMenu {
.py_0()
.px_2()
.rounded_md()
.text_xs()
.text_sm()
.on_mouse_enter(cx.listener(
move |this, _, _window, cx| {
this.hovered_menu_ix = Some(ix);
@@ -677,7 +677,7 @@ impl Render for PopupMenu {
.justify_between()
.child(label.clone())
.child(
IconName::ChevronRight,
IconName::CaretRight,
),
),
)

View File

@@ -198,9 +198,10 @@ impl<T: Styled> StyleSized<T> for T {
fn input_h(self, size: Size) -> Self {
match size {
Size::Large => self.h_11(),
Size::Medium => self.h_8(),
_ => self.h(px(28.)),
Size::Small => self.h_7(),
Size::Medium => self.h_9(),
Size::Large => self.h_12(),
_ => self.h(px(24.)),
}
.input_text_size(size)
}

View File

@@ -81,36 +81,38 @@ impl RenderOnce for Tab {
let (text_color, bg_color, hover_bg_color) = match (self.selected, self.disabled) {
(true, false) => (
cx.theme().base.step(cx, ColorScaleStep::TWELVE),
cx.theme().base.step(cx, ColorScaleStep::THREE),
cx.theme().base.step(cx, ColorScaleStep::FIVE),
cx.theme().base.step(cx, ColorScaleStep::FOUR),
),
(false, false) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().base.step(cx, ColorScaleStep::ONE),
cx.theme().transparent,
cx.theme().base.step(cx, ColorScaleStep::FOUR),
),
// disabled
(true, true) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().base.step(cx, ColorScaleStep::ONE),
cx.theme().transparent,
cx.theme().base.step(cx, ColorScaleStep::FOUR),
),
(false, true) => (
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
cx.theme().base.step(cx, ColorScaleStep::ONE),
cx.theme().transparent,
cx.theme().base.step(cx, ColorScaleStep::FOUR),
),
};
self.base
.h(px(30.))
.px_2()
.relative()
.flex()
.items_center()
.flex_shrink_0()
.cursor_pointer()
.overflow_hidden()
.text_sm()
.text_xs()
.text_ellipsis()
.text_color(text_color)
.bg(bg_color)
.rounded(px(cx.theme().radius))
@@ -118,16 +120,7 @@ impl RenderOnce for Tab {
.when_some(self.prefix, |this, prefix| {
this.child(prefix).text_color(text_color)
})
.child(
div()
.px_3()
.flex()
.items_center()
.gap_1()
.text_ellipsis()
.text_xs()
.child(self.label),
)
.child(self.label)
.when_some(self.suffix, |this, suffix| this.child(suffix))
}
}

View File

@@ -148,7 +148,7 @@ impl Theme {
Theme {
base: color_scales.gray,
accent: color_scales.yellow,
font_size: 16.0,
font_size: 15.0,
font_family: if cfg!(target_os = "macos") {
".SystemUIFont".into()
} else if cfg!(target_os = "windows") {

View File

@@ -1,8 +1,4 @@
use crate::{
h_flex,
theme::{scale::ColorScaleStep, ActiveTheme},
Icon, IconName, InteractiveElementExt as _, Sizable as _,
};
use crate::{h_flex, theme::ActiveTheme, Icon, IconName, InteractiveElementExt as _, Sizable as _};
use gpui::{
black, div, prelude::FluentBuilder as _, px, relative, white, AnyElement, App, ClickEvent, Div,
Element, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
@@ -251,7 +247,7 @@ impl RenderOnce for TitleBar {
.items_center()
.justify_between()
.h(HEIGHT)
.bg(cx.theme().base.step(cx, ColorScaleStep::ONE))
.bg(cx.theme().transparent)
.when(window.is_fullscreen(), |this| this.pl(px(12.)))
.on_double_click(|_, window, _cx| window.zoom_window())
.child(