This commit is contained in:
2026-10-21 16:20:51 +07:00
parent d0ad1a6f57
commit d7160e196e
46 changed files with 2307 additions and 1534 deletions
+7 -8
View File
@@ -10,7 +10,6 @@ use gpui_kit::component::{ActiveTheme, Disableable, WindowExt, v_flex};
use instant::Duration;
use nostr_connect::prelude::*;
use state::NostrRegistry;
use ui::{TextStyle, Typeset as _, surface_radius};
pub fn open(window: &mut Window, cx: &mut App) {
let import = cx.new(|cx| ImportIdentity::new(window, cx));
@@ -239,10 +238,10 @@ impl Render for ImportIdentity {
v_flex()
.size_full()
.gap_4()
.text_style(TextStyle::Callout)
.text_xs()
.child(
v_flex()
.gap(px(ui::SPACE))
.gap_2()
.when(!is_unlock, |this| {
this.child(
v_flex()
@@ -285,9 +284,9 @@ impl Render for ImportIdentity {
this.child(
div()
.p_2()
.rounded(px(surface_radius()))
.rounded(cx.theme().radius_lg)
.bg(cx.theme().warning.opacity(0.15))
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().warning)
.line_height(relative(1.2))
.child(div().child(BUNKER_WARN)),
@@ -297,9 +296,9 @@ impl Render for ImportIdentity {
this.child(
div()
.p_2()
.rounded(px(surface_radius()))
.rounded(cx.theme().radius_lg)
.bg(cx.theme().warning.opacity(0.15))
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().warning)
.line_height(relative(1.2))
.child(div().child(KEY_WARN)),
@@ -330,7 +329,7 @@ impl Render for ImportIdentity {
.when_some(self.error.read(cx).as_ref(), |this, error| {
this.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_center()
.text_color(cx.theme().danger)
.child(error.clone()),
+24 -25
View File
@@ -1,10 +1,3 @@
use crate::nav::Nav;
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use std::ops::Range;
use std::rc::Rc;
@@ -16,11 +9,19 @@ use gpui::{
Render, SharedString, StatefulInteractiveElement as _, Styled, Subscription, Task, WeakEntity,
Window, div, px, uniform_list,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use nostr_sdk::prelude::*;
use person::Directory;
use settings::AppSettings;
use state::NostrRegistry;
use ui::{TextStyle, Typeset as _};
use ui::Avatar;
use crate::nav::Nav;
/// Maximum number of members a chat can have.
const MAX_MEMBERS: usize = 3;
@@ -337,12 +338,13 @@ impl NewChat {
Nav::new(start + offset)
.label(profile.name())
.text_style(TextStyle::Callout)
.text_xs()
.when(!AppSettings::get_hide_avatar(cx), |this| {
this.prefix(match profile.avatar() {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().flex_shrink_0().into_any_element(),
})
this.prefix(
Avatar::new(profile.avatar())
.seed(profile.avatar_seed())
.small(),
)
})
.on_click(cx.listener(move |this, _event, _window, cx| {
this.toggle_member(&public_key, cx);
@@ -367,12 +369,14 @@ impl NewChat {
.p_0p5()
.rounded_full()
.bg(cx.theme().secondary)
.text_style(TextStyle::Caption2)
.text_xs()
.font_medium()
.hover(|this| this.bg(cx.theme().primary))
.child(match profile.avatar() {
Some(picture) => Avatar::new().src(picture).xsmall().into_any_element(),
None => Avatar::new().xsmall().flex_shrink_0().into_any_element(),
})
.child(
Avatar::new(profile.avatar())
.seed(profile.avatar_seed())
.xsmall(),
)
.child(div().max_w_32().truncate().child(profile.name()))
.on_click(cx.listener(move |this, _event, _window, cx| {
this.toggle_member(&public_key, cx);
@@ -411,16 +415,11 @@ impl Render for NewChat {
})
.map(|this| {
if let Some(error) = self.error.clone() {
this.child(
div()
.text_style(TextStyle::Caption)
.text_color(cx.theme().danger)
.child(error),
)
this.child(div().text_xs().text_color(cx.theme().danger).child(error))
} else {
this.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(notice),
)
+20 -23
View File
@@ -7,20 +7,18 @@ use gpui::{
};
use gpui_base::{Popup, Select};
use gpui_kit::assets::IconName;
use gpui_kit::component::ActiveTheme;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::form::field;
use gpui_kit::component::input::{Input, InputEvent, InputState, Textarea, TextareaState};
use gpui_kit::component::separator::Separator;
use gpui_kit::component::switch::Switch;
use gpui_kit::component::{Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry};
use nostr_sdk::prelude::*;
use settings::AppSettings;
use state::upload;
use ui::divider;
use ui::{TextStyle, Typeset as _, button_radius, control_radius};
use ui::Avatar;
const DEFAULT_RELAYS: [&str; 2] = ["wss://relay.ditto.net", "wss://chat.wisp.talk"];
const PRIVATE_DESC: &str = "Only members can read group messages";
@@ -349,7 +347,7 @@ impl NewGroup {
if let Some(parent) = &self.parent {
return v_flex().gap_1p5().child(field_label("Relay", cx)).child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(format!(
"Created on {} (same relay as the parent group).",
@@ -367,8 +365,8 @@ impl NewGroup {
.w_full()
.h_8()
.px_3()
.rounded(px(button_radius()))
.text_style(TextStyle::Callout)
.rounded(cx.theme().radius)
.text_xs()
.cursor_default()
.bg(if open {
cx.theme().list_active
@@ -428,8 +426,8 @@ impl NewGroup {
.id(SharedString::from(format!("option-{relay}")))
.px_1p5()
.py_1()
.rounded(px(button_radius()))
.text_style(TextStyle::Callout)
.rounded(cx.theme().radius)
.text_xs()
.cursor_default()
.hover(|this| this.bg(cx.theme().primary))
.child(relay_label(relay))
@@ -437,14 +435,14 @@ impl NewGroup {
this.activate_relay_option(index, cx);
}))
}))
.child(divider(cx))
.child(Separator::horizontal())
.child(
h_flex()
.id("relay-add")
.px_1p5()
.py_1()
.rounded(px(button_radius()))
.text_style(TextStyle::Callout)
.rounded(cx.theme().radius)
.text_xs()
.cursor_default()
.child("Custom relay")
.hover(|this| this.bg(cx.theme().primary))
@@ -542,7 +540,7 @@ impl NewGroup {
.child(
div()
.flex_1()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().danger)
.child(error.to_owned()),
)
@@ -553,7 +551,7 @@ impl NewGroup {
.cursor_pointer()
.flex_shrink_0()
.underline()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().danger)
.child("Open relay website")
.on_click({
@@ -597,12 +595,11 @@ impl Render for NewGroup {
h_flex()
.items_center()
.gap_3()
.child(match self.picture.clone() {
Some(picture) => {
Avatar::new().src(picture).large().into_any_element()
}
None => Avatar::new().large().into_any_element(),
})
.child(
Avatar::new(self.picture.clone())
.seed(self.name_input.read(cx).value().trim().to_owned())
.large(),
)
.child(
Button::new("upload-picture")
.label(if self.picture.is_some() {
@@ -659,7 +656,7 @@ fn input_field(text: &'static str, input: impl IntoElement, cx: &App) -> Div {
fn field_label(text: &'static str, cx: &App) -> Div {
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(text)
@@ -669,7 +666,7 @@ fn switch_row(switch: impl IntoElement, cx: &App) -> Div {
div()
.w_full()
.p_2()
.rounded(px(control_radius()))
.rounded(cx.theme().radius)
.bg(cx.theme().secondary)
.child(switch)
}
+16 -22
View File
@@ -1,10 +1,3 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::DockPlacement;
use gpui_kit::component::input::{Input, InputState, Textarea, TextareaState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use std::str::FromStr;
use anyhow::Error;
@@ -12,14 +5,20 @@ use chat::{ChatRegistry, Room, RoomKind};
use gpui::prelude::FluentBuilder;
use gpui::{
App, AppContext, ClipboardItem, Context, Div, Entity, IntoElement, ParentElement,
PathPromptOptions, Render, SharedString, Styled, Task, Window, div, px, retain_all,
PathPromptOptions, Render, SharedString, Styled, Task, Window, div, retain_all,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::DockPlacement;
use gpui_kit::component::input::{Input, InputState, Textarea, TextareaState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::{Directory, Person, PersonRegistry, shorten_pubkey};
use settings::AppSettings;
use state::{NostrRegistry, upload};
use ui::{TextStyle, Typeset as _, button_radius};
use ui::Avatar;
use crate::Workspace;
@@ -374,10 +373,7 @@ impl Render for ProfileDialog {
.items_center()
.justify_center()
.bg(cx.theme().sidebar)
.child(match avatar {
Some(picture) => Avatar::new().src(picture).large().into_any_element(),
None => Avatar::new().large().into_any_element(),
})
.child(Avatar::new(avatar).seed(profile.avatar_seed()).large())
.when(self.editing, |this| {
this.child(
Button::new("upload")
@@ -400,9 +396,9 @@ impl Render for ProfileDialog {
.w_full()
.justify_center()
.gap_3()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.bg(cx.theme().sidebar)
.text_style(TextStyle::Callout)
.text_xs()
.child(shorten_pkey)
.child(
Button::new("copy")
@@ -537,7 +533,7 @@ impl Render for ProfileDialog {
fn label(label: &'static str, cx: &App) -> Div {
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.font_weight(gpui::FontWeight::SEMIBOLD)
.child(label)
@@ -554,10 +550,8 @@ fn field(
None => (SharedString::from(placeholder), cx.theme().muted_foreground),
};
v_flex().gap_1p5().child(label(text, cx)).child(
div()
.text_style(TextStyle::Callout)
.text_color(color)
.child(content),
)
v_flex()
.gap_1p5()
.child(label(text, cx))
.child(div().text_xs().text_color(color).child(content))
}
+16 -15
View File
@@ -1,4 +1,5 @@
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem};
@@ -12,13 +13,12 @@ use gpui::prelude::FluentBuilder;
use gpui::{
App, AppContext, ClickEvent, Context, Div, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Stateful, Styled, Subscription, Task, TextAlign,
WeakEntity, Window, div, px, rems,
WeakEntity, Window, div, rems,
};
use instant::Duration;
use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use ui::{TextStyle, Typeset as _, button_radius, panel_radius};
const GOSSIP_MSG: &str = "Gossip Relays are the relays where you publish all your events and discover events from others.";
const INBOX_MSG: &str = "Messaging Relays host your private messages. Others find these relays and send you messages there.";
@@ -108,10 +108,10 @@ impl RelayManager {
let manager = cx.entity().downgrade();
v_flex()
.gap(px(ui::SPACE))
.gap_2()
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(GOSSIP_MSG)),
)
@@ -132,7 +132,7 @@ impl RelayManager {
.tooltip("Relay metadata")
.ghost()
.h(rems(2.))
.text_style(TextStyle::Caption)
.text_xs()
.dropdown_menu(move |menu, _window, _cx| {
menu.item(metadata_item(&manager, RelayMetadata::Read))
.item(metadata_item(&manager, RelayMetadata::Write))
@@ -153,7 +153,7 @@ impl RelayManager {
this.child(
div()
.italic()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().danger)
.child(error.clone()),
)
@@ -197,10 +197,10 @@ impl RelayManager {
fn render_inbox(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap(px(ui::SPACE))
.gap_2()
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(INBOX_MSG)),
)
@@ -224,7 +224,7 @@ impl RelayManager {
this.child(
div()
.italic()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().danger)
.child(error.clone()),
)
@@ -607,7 +607,7 @@ impl Render for RelayManager {
v_flex()
.gap_4()
.text_style(TextStyle::Callout)
.text_xs()
.child(self.render_tab_bar(cx))
.child(content)
.child(self.render_actions(cx))
@@ -646,19 +646,20 @@ fn relay_row(
.h_8()
.px_2()
.justify_between()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.bg(cx.theme().sidebar)
.child(
h_flex()
.gap_1()
.text_style(TextStyle::Callout)
.text_xs()
.child(label)
.when_some(badge, |this, badge| {
this.child(
div()
.p_0p5()
.rounded_xs()
.text_style(TextStyle::Caption2)
.text_xs()
.font_medium()
.child(badge),
)
}),
@@ -681,8 +682,8 @@ fn empty_relays(cx: &App) -> impl IntoElement {
.border_2()
.border_dashed()
.border_color(cx.theme().border)
.rounded(px(panel_radius()))
.text_style(TextStyle::Callout)
.rounded(cx.theme().radius_lg)
.text_xs()
.text_align(TextAlign::Center)
.child(SharedString::from("Please add some relays."))
}
+3 -4
View File
@@ -11,7 +11,6 @@ use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
use instant::Duration;
use nip4e::DeviceRegistry;
use nostr_sdk::prelude::*;
use ui::{TextStyle, Typeset as _};
#[derive(Debug)]
pub struct RestoreEncryption {
@@ -101,11 +100,11 @@ impl Render for RestoreEncryption {
v_flex()
.size_full()
.gap_3()
.text_style(TextStyle::Callout)
.text_xs()
.child(
v_flex()
.gap_1()
.text_style(TextStyle::Callout)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("Secret Key")
.child(Input::new(&self.key_input)),
@@ -125,7 +124,7 @@ impl Render for RestoreEncryption {
.when_some(self.error.read(cx).as_ref(), |this, error| {
this.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_center()
.text_color(cx.theme().danger)
.child(error.clone()),
+39 -43
View File
@@ -1,8 +1,3 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::spinner::Spinner;
use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
use std::collections::HashMap;
use anyhow::Error;
@@ -12,12 +7,16 @@ use gpui::{
App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render,
SharedString, Styled, Subscription, Task, Window, div, px, relative, uniform_list,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::spinner::Spinner;
use gpui_kit::component::{ActiveTheme, Disableable, Icon, Sizable, WindowExt, h_flex, v_flex};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::{Directory, Person, PersonRegistry, shorten_pubkey};
use smallvec::{SmallVec, smallvec};
use state::{BOOTSTRAP_RELAYS, NostrAddress, NostrRegistry, TIMEOUT};
use ui::{TextStyle, Typeset as _, button_radius};
use ui::Avatar;
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<Screening> {
cx.new(|cx| Screening::new(public_key, window, cx))
@@ -311,15 +310,14 @@ impl Screening {
.w_full()
.px_2()
.gap_1p5()
.rounded(px(button_radius()))
.text_style(TextStyle::Callout)
.rounded(cx.theme().radius)
.text_xs()
.hover(|this| this.bg(cx.theme().primary))
.child(match profile.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => Avatar::new().small().into_any_element(),
})
.child(
Avatar::new(profile.avatar())
.seed(profile.avatar_seed())
.small(),
)
.child(profile.name()),
);
}
@@ -371,10 +369,11 @@ impl Render for Screening {
.items_center()
.justify_center()
.text_center()
.child(match profile.avatar() {
Some(picture) => Avatar::new().src(picture).large().into_any_element(),
None => Avatar::new().large().into_any_element(),
})
.child(
Avatar::new(profile.avatar())
.seed(profile.avatar_seed())
.large(),
)
.child(
div()
.font_weight(gpui::FontWeight::SEMIBOLD)
@@ -393,7 +392,7 @@ impl Render for Screening {
.justify_center()
.rounded_full()
.bg(cx.theme().secondary)
.text_style(TextStyle::Callout)
.text_xs()
.truncate()
.text_ellipsis()
.text_center()
@@ -430,36 +429,33 @@ impl Render for Screening {
.child(
h_flex()
.items_start()
.gap(px(ui::SPACE))
.text_style(TextStyle::Callout)
.gap_2()
.text_xs()
.child(status_badge(Some(self.followed), cx))
.child(
v_flex()
.text_style(TextStyle::Callout)
.child("Contact")
.child(
div()
.line_clamp(1)
.text_color(cx.theme().muted_foreground)
.child({
if self.followed {
SharedString::from(CONTACT)
} else {
SharedString::from(NOT_CONTACT)
}
}),
),
v_flex().text_xs().child("Contact").child(
div()
.line_clamp(1)
.text_color(cx.theme().muted_foreground)
.child({
if self.followed {
SharedString::from(CONTACT)
} else {
SharedString::from(NOT_CONTACT)
}
}),
),
),
)
.child(
h_flex()
.items_start()
.gap(px(ui::SPACE))
.text_style(TextStyle::Callout)
.gap_2()
.text_xs()
.child(status_badge(last_active, cx))
.child(
v_flex()
.text_style(TextStyle::Callout)
.text_xs()
.child(
h_flex()
.gap_0p5()
@@ -493,11 +489,11 @@ impl Render for Screening {
.child(
h_flex()
.items_start()
.gap(px(ui::SPACE))
.gap_2()
.child(status_badge(Some(self.verified), cx))
.child(
v_flex()
.text_style(TextStyle::Callout)
.text_xs()
.child({
if let Some(addr) = self.address(cx) {
SharedString::from(format!("{} validation", addr))
@@ -528,11 +524,11 @@ impl Render for Screening {
.child(
h_flex()
.items_start()
.gap(px(ui::SPACE))
.gap_2()
.child(status_badge(Some(mutuals > 0), cx))
.child(
h_flex()
.text_style(TextStyle::Callout)
.text_xs()
.child(
div()
.line_clamp(1)
+7 -14
View File
@@ -4,6 +4,7 @@ use gpui::{
SharedString, Styled, Window, div,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::form::field;
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
@@ -14,7 +15,6 @@ use gpui_kit::component::switch::Switch;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use settings::AppSettings;
use settings::AppearanceMode;
use ui::{TextStyle, Typeset as _};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
cx.new(|cx| Preferences::new(window, cx))
@@ -149,14 +149,10 @@ impl Render for Preferences {
.justify_between()
.child(
v_flex()
.child(div().text_xs().child(SharedString::from("Mode")))
.child(
div()
.text_style(TextStyle::Callout)
.child(SharedString::from("Mode")),
)
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(MODE)),
),
@@ -204,14 +200,10 @@ impl Render for Preferences {
v_flex()
.flex_1()
.min_w_0()
.child(div().text_xs().child(SharedString::from("Image")))
.child(
div()
.text_style(TextStyle::Callout)
.child(SharedString::from("Image")),
)
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.truncate()
.child(SharedString::from(background_label)),
@@ -288,7 +280,8 @@ impl Render for Preferences {
)
.child(
div()
.text_style(TextStyle::Caption2)
.text_xs()
.font_medium()
.italic()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Only support blossom service")),
@@ -0,0 +1,66 @@
use gpui::{
App, Context, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement as _, Render,
SharedString, Styled as _, Window, div,
};
use gpui_base::dock::PanelState;
use gpui_kit::component::ActiveTheme as _;
use gpui_kit::component::dock::{BasePanel, Panel, PanelEvent};
/// Stands in for a panel this build cannot construct.
/// It returns the state it was handed, so the layout survives a load and save.
pub(crate) struct InvalidPanel {
name: SharedString,
focus_handle: FocusHandle,
old_state: PanelState,
}
impl InvalidPanel {
pub(crate) fn new(
name: impl Into<SharedString>,
state: PanelState,
cx: &mut Context<Self>,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
name: name.into(),
old_state: state,
}
}
}
impl BasePanel for InvalidPanel {
fn panel_name(&self) -> &'static str {
"InvalidPanel"
}
fn dump(&self, _: &App) -> PanelState {
self.old_state.clone()
}
}
impl Panel for InvalidPanel {}
impl EventEmitter<PanelEvent> for InvalidPanel {}
impl Focusable for InvalidPanel {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for InvalidPanel {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.my_6()
.flex()
.flex_col()
.items_center()
.justify_center()
.text_color(cx.theme().muted_foreground)
.child(format!(
"The `{}` panel type is not registered in PanelRegistry.",
self.name.clone()
))
}
}
@@ -1,11 +1,21 @@
use std::sync::Arc;
use gpui::{App, WeakEntity, Window};
use gpui::{App, Pixels, WeakEntity, Window, px};
use gpui_kit::component::dock::{
DockArea, DockPlacement, InsertTarget, NodeId, PaneRef, PanelHandle, PanelId,
};
/// The panel in any region of `area` whose id is `key`.
mod invalid_panel;
mod skin;
mod tab_panel;
mod window_controls;
pub use skin::CoopDockSkin;
/// The fixed height of the tab bar, which doubles as the window title bar.
pub const TAB_BAR_HEIGHT: Pixels = px(44.);
/// The panel in any region of `area whose id is `key.
fn find_panel(area: &DockArea, key: PanelId, _cx: &App) -> Option<(PanelId, NodeId, usize)> {
let placements = [
DockPlacement::Center,
@@ -36,7 +46,7 @@ fn find_panel(area: &DockArea, key: PanelId, _cx: &App) -> Option<(PanelId, Node
None
}
/// Add `panel` to the area, or move it to the end of its existing tab group.
/// Add `panel to the area, or move it to the end of its existing tab group.
pub fn add_panel(
area: &mut DockArea,
panel: PanelHandle,
+269
View File
@@ -0,0 +1,269 @@
use std::cell::Cell;
use std::ops::Deref as _;
use std::rc::Rc;
use std::sync::Arc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, AppContext as _, Axis, Context, Div, Element, Empty, InteractiveElement as _,
IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Render, Stateful, Style,
Styled as _, WeakEntity, Window, div, px,
};
use gpui_base::dock::{
DockArea, DockAreaRenderer, DockContext, DockEvent, DockPlacement, NodeId, PanelState,
PanelView, TabGroupRenderer,
};
use gpui_base::{HandleEdge, resize_handle};
use gpui_kit::component::ActiveTheme as _;
use gpui_kit::component::dock::panel_handle;
use super::invalid_panel::InvalidPanel;
use super::tab_panel::CoopTabGroupSkin;
/// State the skin shares with its per-container renderers.
pub(crate) struct SkinShared {
area: WeakEntity<DockArea>,
toggle_button_visible: Cell<bool>,
/// The dock whose resize handle is being dragged, if any. Only one can be.
resizing_dock: Cell<Option<DockPlacement>>,
}
impl SkinShared {
pub(crate) fn area(&self) -> &WeakEntity<DockArea> {
&self.area
}
pub(crate) fn is_toggle_button_visible(&self) -> bool {
self.toggle_button_visible.get()
}
pub(crate) fn resizing_dock(&self) -> &Cell<Option<DockPlacement>> {
&self.resizing_dock
}
}
/// The coop appearance for a [`DockArea].
/// Install it in the constructor, the only place the area's weak handle is available.
pub struct CoopDockSkin {
shared: Rc<SkinShared>,
}
impl CoopDockSkin {
pub fn new(cx: &mut Context<DockArea>) -> Rc<Self> {
Rc::new(Self {
shared: Rc::new(SkinShared {
area: cx.weak_entity(),
toggle_button_visible: Cell::new(true),
resizing_dock: Cell::new(None),
}),
})
}
pub(crate) fn shared(&self) -> &Rc<SkinShared> {
&self.shared
}
}
/// Payload a dock's resize handle drags.
///
/// It draws nothing, the handle element is the visible affordance.
#[derive(Clone)]
struct ResizePanel;
impl Render for ResizePanel {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
Empty
}
}
impl DockAreaRenderer for CoopDockSkin {
fn frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
div()
.id("dock-area")
.relative()
.size_full()
.overflow_hidden()
.flex()
.flex_row()
}
fn center_frame(&self, _: &mut Window, _: &mut App) -> Stateful<Div> {
div()
.id("dock-area-center")
.flex()
.flex_1()
.flex_col()
.overflow_hidden()
}
fn split_frame(&self, node: NodeId, _: Axis, _: &mut Window, cx: &mut App) -> Stateful<Div> {
// `size_full and `flex_1 stop the frame collapsing in an unsizing parent.
div()
.id(("dock-split-frame", node.as_u64()))
.size_full()
.flex_1()
.min_h(px(0.))
.overflow_hidden()
.bg(cx.theme().tokens.tab_bar)
}
fn render_dock(
&self,
dock: &DockContext,
content: AnyElement,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
div()
.flex()
.size_full()
.relative()
.child(content)
.child(self.render_resize_handle(dock, window, cx))
.child(DockResizeTracker {
dock: dock.clone(),
shared: self.shared().clone(),
})
.into_any_element()
}
/// Placeholder for a panel this build cannot construct.
/// It dumps the state it was handed, so the layout survives a load and save.
fn build_placeholder(
&self,
state: &PanelState,
_: &mut Window,
cx: &mut App,
) -> Option<Arc<dyn PanelView>> {
let state = state.clone();
Some(panel_handle(cx.new(|cx| {
InvalidPanel::new(state.panel_name.clone(), state, cx)
})))
}
fn tab_group_renderer(&self) -> Rc<dyn TabGroupRenderer> {
Rc::new(CoopTabGroupSkin::new(self.shared().clone()))
}
}
impl CoopDockSkin {
fn render_resize_handle(
&self,
dock: &DockContext,
_: &mut Window,
_: &mut App,
) -> impl IntoElement {
let placement = dock.placement();
let shared = self.shared().clone();
resize_handle("resize-handle", placement.axis())
.when(placement.is_left(), |this| {
this.inside(HandleEdge::Trailing)
})
.when(placement.is_right() || placement.is_bottom(), |this| {
this.inside(HandleEdge::Leading)
})
.on_drag(ResizePanel, move |info, _, _, cx| {
cx.stop_propagation();
shared.resizing_dock().set(Some(placement));
cx.new(|_| info.deref().clone())
})
}
}
/// Turns the window's mouse stream into dock resizing.
/// It draws nothing, the `paint hook is the only window listener registration point.
struct DockResizeTracker {
dock: DockContext,
shared: Rc<SkinShared>,
}
impl IntoElement for DockResizeTracker {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for DockResizeTracker {
type PrepaintState = ();
type RequestLayoutState = ();
fn id(&self) -> Option<gpui::ElementId> {
None
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
(window.request_layout(Style::default(), None, cx), ())
}
fn prepaint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
_: gpui::Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Window,
_: &mut App,
) -> Self::PrepaintState {
}
fn paint(
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
_: gpui::Bounds<Pixels>,
_: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
_: &mut App,
) {
let placement = self.dock.placement();
window.on_mouse_event({
let dock = self.dock.clone();
let shared = self.shared.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
return;
}
// Dragging a closed dock's handle reopens it.
// Read the live state, the snapshot in `dock would toggle it shut again.
let open = shared
.area()
.upgrade()
.is_some_and(|area| area.read(cx).is_dock_open(placement));
if !open {
dock.toggle(window, cx);
}
dock.resize_to(event.position, window, cx);
}
});
window.on_mouse_event({
let shared = self.shared.clone();
move |_: &MouseUpEvent, phase, _, cx| {
if !phase.bubble() || shared.resizing_dock().get() != Some(placement) {
return;
}
shared.resizing_dock().set(None);
// The size lives on the dock, not the layout tree.
// Nothing else tells a subscriber to persist it.
_ = shared
.area()
.update(cx, |_, cx| cx.emit(DockEvent::LayoutChanged));
}
});
}
}
+779
View File
@@ -0,0 +1,779 @@
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
use gpui::prelude::FluentBuilder as _;
use gpui::{
Animation, AnimationExt as _, AnyElement, AnyView, App, AppContext as _, Bounds, Context, Div,
Empty, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Point, Render,
ScrollHandle, SharedString, Stateful, StatefulInteractiveElement as _, StyleRefinement,
Styled as _, Window, div, px, size,
};
use gpui_base::dock::{
AnyDrag, DockPlacement, DragPanel, DropIndicator, NodeId, PaneNode, PaneRef,
PanelView as BasePanelView, TabGroupContext, TabGroupRenderer,
};
use gpui_base::{ElementExt, InteractiveElementExt, Tab, Tabs};
use gpui_kit::assets::IconName;
use gpui_kit::component::animation::{Lerp as _, ease_out_cubic};
use gpui_kit::component::button::{Button, ButtonVariants as _};
use gpui_kit::component::dock::{ClosePanel, PanelControl, PanelHandle, ToggleZoom};
use gpui_kit::component::{ActiveTheme as _, Sizable as _, h_flex, v_flex};
use ui::title_bar_drag_handlers;
use super::skin::SkinShared;
use super::{TAB_BAR_HEIGHT, window_controls};
const DRAG_PREVIEW_SIZE: gpui::Size<gpui::Pixels> = size(px(96.), px(30.));
const ACTIVE_TAB_SELECTOR: &str = "coop-dock-active-tab";
const ACTIVE_TAB_CLOSE_SELECTOR: &str = "coop-dock-active-tab-close";
const TAB_HOVER_GROUP: &str = "coop-dock-tab";
/// A panel's title, or its registered name when the panel has no handle.
pub(crate) fn panel_title(
panel: &Arc<dyn BasePanelView>,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
match PanelHandle::of(panel) {
Some(handle) => handle.title(window, cx),
None => SharedString::from(panel.panel_name(cx)).into_any_element(),
}
}
/// The preview that follows the cursor while a panel is dragged.
struct DragPanelPreview {
panel: Arc<dyn BasePanelView>,
}
impl Render for DragPanelPreview {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.id("drag-panel")
.cursor_grab()
.p_1()
.whitespace_nowrap()
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius_lg)
.text_color(cx.theme().tab_foreground)
.bg(cx.theme().tokens.tab_active)
.opacity(0.75)
.child(panel_title(&self.panel, window, cx))
}
}
/// The zoom affordance `panel offers, if any.
///
/// The panel must offer a control and be zoomable, base refuses a zoom otherwise.
fn panel_zoom_control(panel: &Arc<dyn BasePanelView>, cx: &App) -> Option<PanelControl> {
panel
.zoomable(cx)
.then(|| PanelHandle::of(panel).and_then(|handle| handle.zoom_control(cx)))
.flatten()
}
/// The left-most, top-most tab group in a container.
///
/// A left dock's collapse button lives in this group.
fn left_top_group(node: &PaneNode) -> Option<NodeId> {
match node.kind() {
PaneRef::Tabs { .. } => Some(node.id()),
PaneRef::Split { children, .. } => children.first().and_then(left_top_group),
}
}
/// The right-most, top-most tab group.
///
/// A vertical split picks its first child, a horizontal split picks its last.
fn right_top_group(node: &PaneNode) -> Option<NodeId> {
match node.kind() {
PaneRef::Tabs { .. } => Some(node.id()),
PaneRef::Split { axis, children, .. } => match axis {
gpui::Axis::Vertical => children.first(),
gpui::Axis::Horizontal => children.last(),
}
.and_then(right_top_group),
}
}
/// The bottom or right dock whose root tab group is this one, if any.
///
/// Base keeps a dock's last group, so the skin removes these docks as a whole.
fn dock_root_group(
shared: &SkinShared,
group: &TabGroupContext,
cx: &App,
) -> Option<DockPlacement> {
let area = shared.area().upgrade()?;
let area = area.read(cx);
[DockPlacement::Bottom, DockPlacement::Right]
.into_iter()
.find(|placement| {
area.layout(*placement)
.is_some_and(|tree| tree.root().id() == group.node())
})
}
/// Close `panel, removing its dock instead when base would refuse an empty one.
///
/// A bottom or right dock's only panel is that case, so the whole dock goes.
fn close_panel(
shared: &SkinShared,
group: &TabGroupContext,
panel: &Arc<dyn BasePanelView>,
window: &mut Window,
cx: &mut App,
) {
if !panel.closable(cx) {
return;
}
let dock = (group.panels().len() <= 1)
.then(|| dock_root_group(shared, group, cx))
.flatten();
match dock {
Some(placement) => {
_ = shared.area().update(cx, |area, cx| {
area.remove_dock(placement, window, cx);
});
}
None => group.close(panel.panel_id(cx), window, cx),
}
}
/// One tab group's appearance, built once per container so its geometry is its own.
pub(crate) struct CoopTabGroupSkin {
shared: Rc<SkinShared>,
scroll_handle: ScrollHandle,
/// The tab shown last frame, so a change scrolls the new one into view.
last_active_ix: Cell<Option<usize>>,
/// Bounds of the title bar row, measured to place the title-bar drag overlay.
title_bar_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
/// Bounds of the empty strip after the last tab, where the drag region starts.
title_bar_strip_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
/// Bounds of the suffix area, where the drag region ends.
title_bar_suffix_bounds: Rc<Cell<Option<Bounds<Pixels>>>>,
}
impl CoopTabGroupSkin {
pub(crate) fn new(shared: Rc<SkinShared>) -> Self {
Self {
shared,
scroll_handle: ScrollHandle::default(),
last_active_ix: Cell::new(None),
title_bar_bounds: Rc::new(Cell::new(None)),
title_bar_strip_bounds: Rc::new(Cell::new(None)),
title_bar_suffix_bounds: Rc::new(Cell::new(None)),
}
}
/// A group that is the left dock's only group, with one panel, draws no chrome.
/// The sidebar is one.
fn is_plain_sidebar_group(&self, group: &TabGroupContext, cx: &mut App) -> bool {
let Some(area) = self.shared.area().upgrade() else {
return false;
};
let area = area.read(cx);
let Some(left) = area
.layout(DockPlacement::Left)
.map(|tree| tree.root().id())
else {
return false;
};
left == group.node() && group.panels().len() == 1
}
/// The tab's drag payload, or `None when the group must not be rearranged.
///
/// A locked group never is, a bottom or right dock root always is.
fn tab_drag(&self, group: &TabGroupContext, ix: usize, cx: &App) -> Option<DragPanel> {
if group.is_locked() {
return None;
}
if !group.is_draggable() && dock_root_group(&self.shared, group, cx).is_none() {
return None;
}
group.drag_panel(ix, cx)
}
/// A dock's collapse button for this group's bar, or `None when it does not belong.
/// The icon direction depends on whether the dock is open.
fn dock_toggle_button(
&self,
placement: DockPlacement,
group: &TabGroupContext,
cx: &mut App,
) -> Option<Button> {
if group.is_zoomed() || !self.shared.is_toggle_button_visible() {
return None;
}
let area = self.shared.area().upgrade()?;
let area = area.read(cx);
// A missing dock is not collapsible, this also covers the old `left_dock.is_some() test.
if !area.is_dock_collapsible(placement) {
return None;
}
let designated = match placement {
DockPlacement::Left => area
.layout(DockPlacement::Center)
.and_then(|tree| left_top_group(tree.root())),
DockPlacement::Right => area
.layout(DockPlacement::Center)
.and_then(|tree| right_top_group(tree.root())),
DockPlacement::Bottom => area
.layout(DockPlacement::Bottom)
.and_then(|tree| left_top_group(tree.root())),
DockPlacement::Center => return None,
};
if designated != Some(group.node()) {
return None;
}
let is_open = area.is_dock_open(placement);
let icon = match (placement, is_open) {
(DockPlacement::Left, true) => IconName::PanelLeft,
(DockPlacement::Left, false) => IconName::PanelLeftOpen,
(DockPlacement::Right, true) => IconName::PanelRight,
(DockPlacement::Right, false) => IconName::PanelRightOpen,
(DockPlacement::Bottom, true) => IconName::PanelBottom,
(DockPlacement::Bottom, false) => IconName::PanelBottomOpen,
(DockPlacement::Center, _) => return None,
};
let area = self.shared.area().clone();
Some(
Button::new(SharedString::from(format!("toggle-dock:{placement:?}")))
.icon(icon)
.small()
.ghost()
.tab_stop(false)
.tooltip(match is_open {
true => "Collapse",
false => "Expand",
})
.on_click(move |_, window, cx| {
_ = area.update(cx, |area, cx| area.toggle_dock(placement, window, cx));
}),
)
}
/// One tab of the pill strip.
#[allow(clippy::too_many_arguments)]
fn render_tab(
&self,
group: &TabGroupContext,
ix: usize,
panel: Arc<dyn BasePanelView>,
active: bool,
is_bottom_dock: bool,
window: &mut Window,
cx: &mut App,
) -> Tab {
let collapsed = group.is_collapsed();
let droppable = group.is_droppable();
let drag = self.tab_drag(group, ix, cx);
let handle = PanelHandle::of(&panel);
let id = SharedString::from(format!("close-tab:{}:{ix}", group.node().as_u64()));
let closable = !collapsed
&& (group.is_closable()
|| (dock_root_group(&self.shared, group, cx).is_some() && panel.closable(cx)));
// Whether a double-click zooms this tab in.
let can_zoom_in = panel_zoom_control(&panel, cx).is_some();
Tab::new(ix)
.group(TAB_HOVER_GROUP)
.when(active, |this| {
this.debug_selector(|| ACTIVE_TAB_SELECTOR.to_string())
})
.flex()
.items_center()
.flex_shrink_0()
.h_7()
.min_w_40()
.gap_1()
.px_1p5()
.text_sm()
.overflow_hidden()
.rounded(cx.theme().radius)
.text_color(cx.theme().tab_foreground)
.child(
h_flex()
.flex_1()
.h_6()
.items_center()
.overflow_hidden()
.whitespace_nowrap()
.map(|this| match handle.and_then(|handle| handle.tab_name(cx)) {
Some(tab_name) => this.child(tab_name),
None => this.child(panel_title(&panel, window, cx)),
}),
)
.when(closable, |this| {
this.child(
div()
.flex_shrink_0()
.when(active, |this| {
this.debug_selector(|| ACTIVE_TAB_CLOSE_SELECTOR.to_string())
})
.when(!active, |this| {
this.invisible()
.group_hover(TAB_HOVER_GROUP, |this| this.visible())
})
.child(
Button::new(id)
.icon(IconName::Close)
.tooltip("Close")
.ghost()
.xsmall()
.tab_stop(false)
.on_click({
let group = group.clone();
let shared = self.shared.clone();
let panel = panel.clone();
move |_, window, cx| {
cx.stop_propagation();
close_panel(&shared, &group, &panel, window, cx);
}
}),
),
)
})
.styles(|styles| {
styles.selected(|style| {
style
.text_color(cx.theme().tab_active_foreground)
.bg(cx.theme().tab_active)
})
})
.hover(|this| {
if active {
this
} else {
this.text_color(cx.theme().tab_foreground)
.bg(cx.theme().tab)
}
})
.selected(active)
.on_click({
let group = group.clone();
let area = self.shared.area().clone();
move |event, window, cx| {
group.select_tab(ix, window, cx);
// Clicking the strip of a collapsed bottom dock reopens it.
if is_bottom_dock && collapsed {
_ = area.update(cx, |area, cx| {
area.toggle_dock(DockPlacement::Bottom, window, cx);
});
return;
}
// A double-click zooms the tab's panel
if event.click_count() < 2 || (!group.is_zoomed() && !can_zoom_in) {
return;
}
group.toggle_zoom(window, cx);
}
})
.when(!collapsed, |this| {
this.when_some(drag, |this, drag| {
this.on_drag(drag, {
let panel = panel.clone();
move |drag, offset, _, cx| {
cx.stop_propagation();
drag.set_drag_offset(offset);
drag.set_preview_size(DRAG_PREVIEW_SIZE);
cx.new(|_| DragPanelPreview {
panel: panel.clone(),
})
}
})
})
.when(droppable, |this| {
this.drag_over::<DragPanel>(|this, _, _, cx| {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
})
.on_drop({
let group = group.clone();
move |drag: &DragPanel, window, cx| {
group.drop_panel(drag.clone(), Some(ix), true, window, cx);
}
})
.drag_over::<AnyDrag>(|this, _, _, cx| {
this.rounded_l_none()
.border_l_2()
.border_r_0()
.border_color(cx.theme().drag_border)
})
.on_drop({
let group = group.clone();
move |item: &AnyDrag, window, cx| {
group.drop_item(item.clone(), None, window, cx);
}
})
})
})
}
/// The strip after the last tab, a drop target for panels and other drag items.
/// Its left edge marks where the title-bar drag overlay starts.
fn render_empty_space(
&self,
group: &TabGroupContext,
tabs_count: usize,
_cx: &mut App,
) -> AnyElement {
let strip_bounds = self.title_bar_strip_bounds.clone();
let shared = self.shared.clone();
let droppable = group.is_droppable();
let mut empty = div()
.id("tab-bar-empty-space")
.h_full()
.flex_grow_1()
.min_w_16()
.on_prepaint(move |bounds, _, cx| {
if strip_bounds.get() != Some(bounds) {
strip_bounds.set(Some(bounds));
_ = shared.area().update(cx, |_, cx| cx.notify());
}
});
if droppable {
empty = empty
.drag_over::<DragPanel>(|this, _, _, cx| this.bg(cx.theme().tokens.drop_target))
.on_drop({
let group = group.clone();
let node = group.node();
move |drag: &DragPanel, window, cx| {
// A panel dropped past its own last tab lands in the final slot.
// A panel from elsewhere is appended in the background.
let ix = (drag.source() == node).then(|| tabs_count - 1);
group.drop_panel(drag.clone(), ix, false, window, cx);
}
})
.drag_over::<AnyDrag>(|this, _, _, cx| this.bg(cx.theme().tokens.drop_target))
.on_drop({
let group = group.clone();
move |item: &AnyDrag, window, cx| {
group.drop_item(item.clone(), None, window, cx);
}
});
}
empty.into_any_element()
}
}
impl TabGroupRenderer for CoopTabGroupSkin {
fn frame(&self, group: &TabGroupContext, _: &mut Window, cx: &mut App) -> Stateful<Div> {
let control = group
.active_panel()
.and_then(|panel| panel_zoom_control(panel, cx));
// An emptied group draws nothing, so no bare tab bar is left behind.
if group.panels().is_empty() {
return div().id("tab-panel");
}
let shared = self.shared.clone();
// `v_flex, a plain `div ignores `flex_grow and the content would collapse.
v_flex()
.id("tab-panel")
.size_full()
.overflow_hidden()
.bg(cx.theme().tokens.background)
.when(!group.is_collapsed(), |this| {
this.on_action({
let group = group.clone();
move |_: &ToggleZoom, window, cx| {
if !group.is_zoomed() && control.is_none() {
return;
}
group.toggle_zoom(window, cx);
}
})
.on_action({
let group = group.clone();
let shared = shared.clone();
move |_: &ClosePanel, window, cx| {
let Some(panel) = group.active_panel() else {
return;
};
close_panel(&shared, &group, panel, window, cx);
}
})
})
}
fn content_frame(&self, group: &TabGroupContext, _: &mut Window, _: &mut App) -> Stateful<Div> {
v_flex()
.id("active-panel")
// A collapsed group draws its tab strip only, so the content claims no space.
.when(!group.is_collapsed(), |this| this.flex_1())
}
fn render_tab_bar(
&self,
group: &TabGroupContext,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
// An emptied group draws no tab bar, the app prunes the emptied dock later.
if group.panels().is_empty() {
return Empty.into_any_element();
}
// The sidebar group draws no chrome, the sidebar carries its own header.
if self.is_plain_sidebar_group(group, cx) {
return Empty.into_any_element();
}
let collapsed = group.is_collapsed();
let active_ix = group.active_ix();
let tabs_count = group.panels().len();
let left_dock_button = self.dock_toggle_button(DockPlacement::Left, group, cx);
let bottom_dock_button = self.dock_toggle_button(DockPlacement::Bottom, group, cx);
let right_dock_button = self.dock_toggle_button(DockPlacement::Right, group, cx);
let is_bottom_dock = bottom_dock_button.is_some();
// On macOS the traffic lights overlay the window's top-left corner, so the
// tab bar under them reserves the space. That is the center's top-left group
// when the left dock is closed or absent, and the zoomed group otherwise:
// a zoom fills the whole area, hiding the docks the frame would draw.
let needs_traffic_light_padding = cfg!(target_os = "macos")
&& self.shared.area().upgrade().is_some_and(|area| {
let area = area.read(cx);
match area.zoomed_group() {
Some(zoomed) => zoomed == group.node(),
None => {
!area.is_dock_open(DockPlacement::Left)
&& area
.layout(DockPlacement::Center)
.and_then(|tree| left_top_group(tree.root()))
== Some(group.node())
}
}
});
// Bring a newly displayed tab into view.
// The group owns selection, so the skin watches for the change itself.
let displayed = group.active_panel().map(|panel| panel.panel_id(cx));
let visible: Vec<usize> = group
.panels()
.iter()
.enumerate()
.filter(|(_, panel)| panel.visible(cx))
.map(|(ix, _)| ix)
.collect();
if self.last_active_ix.replace(Some(active_ix)) != Some(active_ix)
&& let Some(visible_ix) = visible.iter().position(|ix| *ix == active_ix)
{
self.scroll_handle.scroll_to_item(visible_ix);
}
// The tab strip ends at the last tab, the bar has no element after it.
// Cover that dead zone with an overlay so it can drag the window.
let drag_overlay = match (
self.title_bar_bounds.get(),
self.title_bar_strip_bounds.get(),
self.title_bar_suffix_bounds.get(),
) {
(Some(title), Some(strip), Some(suffix)) => {
let left = strip.left() - title.left();
let right = title.right() - suffix.left();
(left + right < title.size.width).then(|| {
title_bar_drag_handlers(
div()
.id(("title-bar-drag", group.node().as_u64()))
.absolute()
.top_0()
.bottom_0()
.left(left)
.right(right),
window,
cx,
)
})
}
_ => None,
};
let tabs: Vec<_> = group
.panels()
.iter()
.enumerate()
.filter_map(|(ix, panel)| {
let mut active = displayed == Some(panel.panel_id(cx));
if !panel.visible(cx) {
return None;
}
// Collapsed tabs never show as active, the strip only reopens the dock.
if collapsed {
active = false;
}
Some(self.render_tab(group, ix, panel.clone(), active, is_bottom_dock, window, cx))
})
.collect();
let empty_space = self.render_empty_space(group, tabs_count, cx);
let title_bar_bounds = self.title_bar_bounds.clone();
let title_bar_suffix_bounds = self.title_bar_suffix_bounds.clone();
let shared = self.shared.clone();
let suffix_shared = self.shared.clone();
div()
.flex()
.flex_row()
.child(
div()
.relative()
.flex_1()
.min_w_0()
.on_prepaint(move |bounds, _, cx| {
if title_bar_bounds.get() != Some(bounds) {
title_bar_bounds.set(Some(bounds));
_ = shared.area().update(cx, |_, cx| cx.notify());
}
})
.child(
Tabs::new("tab-bar")
.px(px(-1.))
// A collapsed group stands in for a closed bottom dock,
// which base gives only its reserved strip. The bar keeps
// its own height there rather than the title-bar height,
// so it fits instead of being clipped away.
.when(!collapsed, |this| this.h(TAB_BAR_HEIGHT))
.flex()
.items_center()
.text_color(cx.theme().tab_foreground)
.child(
h_flex()
.items_center()
.top_0()
// -1 px so the border does not overlap the first tab.
.right(-px(1.))
.h_full()
.gap_2()
.px_2()
.when(needs_traffic_light_padding, |this| this.pl(px(80.)))
.children(left_dock_button)
.children(bottom_dock_button),
)
.child(
h_flex().id("tabs").flex_1().overflow_x_hidden().child(
h_flex()
.id("tabs-inner")
.relative()
.gap(px(4.))
.overflow_x_scroll()
.lock_scroll_axis()
.track_scroll(&self.scroll_handle)
.children(tabs)
.when(!collapsed, |this| this.child(empty_space)),
),
)
.when(!collapsed, |this| {
this.child(
h_flex()
.items_center()
.top_0()
.right_0()
.h_full()
.px_2()
.gap_1()
.on_prepaint(move |bounds, _, cx| {
if title_bar_suffix_bounds.get() != Some(bounds) {
title_bar_suffix_bounds.set(Some(bounds));
_ = suffix_shared
.area()
.update(cx, |_, cx| cx.notify());
}
})
.children(
group
.active_panel()
.and_then(PanelHandle::of)
.and_then(|handle| handle.title_suffix(window, cx)),
)
.children(right_dock_button),
)
}),
)
.when_some(drag_overlay, |this, overlay| this.child(overlay)),
)
.child(window_controls::window_controls(window, cx))
.into_any_element()
}
fn render_active_panel(
&self,
panel: AnyView,
group: &TabGroupContext,
_: &mut Window,
_: &mut App,
) -> AnyElement {
if group.is_collapsed() {
return Empty.into_any_element();
}
div()
.id("tab-content")
.overflow_y_scroll()
.overflow_x_hidden()
.flex_1()
.child(panel.cached(StyleRefinement::default().absolute().size_full()))
.into_any_element()
}
fn render_drop_indicator(
&self,
indicator: DropIndicator,
_: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
let (from, to) = (indicator.from(), indicator.to());
// The element sits at the drop target, the animation walks back from the source.
let offset = from.origin() - to.origin();
Some(
div()
.absolute()
.left(to.origin().x)
.top(to.origin().y)
.w(to.size().width)
.h(to.size().height)
.child(
div()
.absolute()
.bg(cx.theme().tokens.drop_target)
.with_animation(
gpui::ElementId::NamedInteger(
"drop-placeholder".into(),
indicator.epoch(),
),
Animation::new(Duration::from_millis(150)).with_easing(ease_out_cubic),
move |this, delta| {
let origin = offset.lerp(&Point::default(), delta);
let width = from.size().width.lerp(&to.size().width, delta);
let height = from.size().height.lerp(&to.size().height, delta);
this.left(origin.x).top(origin.y).w(width).h(height)
},
),
)
.into_any_element(),
)
}
}
@@ -0,0 +1,169 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
App, Hsla, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
StatefulInteractiveElement, Styled, Window, div, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::{ActiveTheme as _, Icon, Sizable as _, h_flex};
use super::TAB_BAR_HEIGHT;
const CONTROL_WIDTH: f32 = 34.;
#[derive(IntoElement, Clone)]
enum ControlIcon {
Minimize,
Restore,
Maximize,
Close,
}
impl ControlIcon {
fn minimize() -> Self {
Self::Minimize
}
fn restore() -> Self {
Self::Restore
}
fn maximize() -> Self {
Self::Maximize
}
fn close() -> Self {
Self::Close
}
fn id(&self) -> &'static str {
match self {
Self::Minimize => "minimize",
Self::Restore => "restore",
Self::Maximize => "maximize",
Self::Close => "close",
}
}
fn icon(&self) -> IconName {
match self {
Self::Minimize => IconName::WindowMinimize,
Self::Restore => IconName::WindowRestore,
Self::Maximize => IconName::WindowMaximize,
Self::Close => IconName::WindowClose,
}
}
fn window_control_area(&self) -> gpui::WindowControlArea {
match self {
Self::Minimize => gpui::WindowControlArea::Min,
Self::Restore | Self::Maximize => gpui::WindowControlArea::Max,
Self::Close => gpui::WindowControlArea::Close,
}
}
fn is_close(&self) -> bool {
matches!(self, Self::Close)
}
#[inline]
fn hover_fg(&self, cx: &App) -> Hsla {
if self.is_close() {
cx.theme().danger_foreground
} else {
cx.theme().secondary_foreground
}
}
#[inline]
fn hover_bg(&self, cx: &App) -> Hsla {
if self.is_close() {
cx.theme().danger
} else {
cx.theme().secondary_hover
}
}
#[inline]
fn active_bg(&self, cx: &mut App) -> Hsla {
if self.is_close() {
cx.theme().danger_active
} else {
cx.theme().secondary_active
}
}
}
impl RenderOnce for ControlIcon {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let is_linux = cfg!(target_os = "linux");
let is_windows = cfg!(target_os = "windows");
let hover_fg = self.hover_fg(cx);
let hover_bg = self.hover_bg(cx);
let active_bg = self.active_bg(cx);
let icon = self.clone();
div()
.id(self.id())
.flex()
.w(px(CONTROL_WIDTH))
.h_full()
.flex_shrink_0()
.justify_center()
.content_center()
.items_center()
.text_color(cx.theme().foreground)
.hover(|style| style.bg(hover_bg).text_color(hover_fg))
.active(|style| style.bg(active_bg).text_color(hover_fg))
.when(is_windows, |this| {
this.window_control_area(self.window_control_area())
})
.when(is_linux, |this| {
this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
window.prevent_default();
cx.stop_propagation();
})
.on_click(move |_, window, cx| {
cx.stop_propagation();
match icon {
Self::Minimize => window.minimize_window(),
Self::Restore | Self::Maximize => window.zoom_window(),
Self::Close => window.remove_window(),
}
})
})
.child(Icon::new(self.icon()).small())
}
}
pub(crate) fn window_controls(window: &mut Window, cx: &mut App) -> impl IntoElement {
if cfg!(target_os = "macos") || cfg!(target_family = "wasm") {
return div().id("window-controls");
}
let supported = window.window_controls();
h_flex()
.id("window-controls")
.items_center()
.flex_shrink_0()
.h_full()
// The controls span the title bar but never grow past the tab bar height.
.when(cfg!(target_os = "windows"), |this| {
this.max_h(TAB_BAR_HEIGHT)
})
.border_l_1()
.border_b_1()
.border_color(cx.theme().border)
.bg(cx.theme().tokens.tab_bar)
.when(supported.minimize, |this| {
this.child(ControlIcon::minimize())
})
.when(supported.maximize, |this| {
this.child(if window.is_maximized() {
ControlIcon::restore()
} else {
ControlIcon::maximize()
})
})
.child(ControlIcon::close())
}
+11 -5
View File
@@ -6,7 +6,7 @@ use chat::{ChatEvent, ChatRegistry};
use common::download_dir;
use gpui::{
Action, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, Styled, Subscription, Task, Window, div, px,
Pixels, Render, SharedString, Styled, Subscription, Task, Window, div, px,
};
use gpui_kit::component::button::Button;
use gpui_kit::component::dock::{DockArea, DockLayout, DockPlacement, Panel, PanelHandle};
@@ -18,7 +18,6 @@ use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent};
use ui::{SIDEBAR_WIDTH, TextStyle, Typeset as _};
use crate::dialogs::restore::RestoreEncryption;
use crate::dialogs::{import, join_group, new_chat, new_group, profile, relays, settings};
@@ -31,6 +30,11 @@ mod nav;
mod panels;
mod sidebar;
pub use dock::TAB_BAR_HEIGHT;
/// Default sidebar width.
const SIDEBAR_WIDTH: Pixels = px(240.);
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
cx.new(|cx| Workspace::new(window, cx))
}
@@ -74,8 +78,10 @@ impl Workspace {
let device = DeviceRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let (dock, _skin) =
gpui_kit::component::dock::DockSkin::dock_area("coop", None, window, cx);
let dock = cx.new(|cx| {
let skin = dock::CoopDockSkin::new(cx);
gpui_kit::component::dock::DockArea::new("coop", None, window, cx).with_renderer(skin)
});
let sidebar = cx.new(|cx| Sidebar::new(window, dock.downgrade(), cx));
let left_sidebar = sidebar.clone();
@@ -438,7 +444,7 @@ impl Workspace {
.description(
v_flex()
.gap_1()
.text_style(TextStyle::Callout)
.text_xs()
.child(SharedString::from(ENC_MSG))
.child(
div()
+2 -3
View File
@@ -4,10 +4,9 @@ use gpui::prelude::FluentBuilder as _;
use gpui::{
AnyElement, App, ClickEvent, ElementId, InteractiveElement, Interactivity, IntoElement,
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
Styled, Window, div, px,
Styled, Window, div,
};
use gpui_kit::component::{ActiveTheme, Selectable, StyledExt as _, h_flex, v_flex};
use ui::button_radius;
/// A single navigation row, such as an entry in a sidebar list.
#[derive(IntoElement)]
@@ -115,7 +114,7 @@ impl RenderOnce for Nav {
.w_full()
.px_1p5()
.gap_1p5()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.text_color(text_color)
.when_some(self.prefix, |this, prefix| this.child(prefix))
.child(
+8 -9
View File
@@ -1,18 +1,17 @@
use anyhow::Error;
use gpui::{
App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, px,
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{Input, InputState};
use gpui_kit::component::separator::Separator;
use gpui_kit::component::{ActiveTheme, Sizable, v_flex};
use instant::Duration;
use nostr_sdk::prelude::*;
use state::USER_KEYRING;
use ui::divider;
use ui::{TextStyle, Typeset as _};
const MSG: &str = "Store your account keys in a safe location. \
You can restore your account or move to another client anytime you want.";
@@ -136,24 +135,24 @@ impl Render for BackupPanel {
.w_full()
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(MSG)),
)
.child(divider(cx))
.child(Separator::horizontal())
.child(
v_flex()
.gap(px(ui::SPACE))
.gap_2()
.flex_1()
.w_full()
.text_style(TextStyle::Callout)
.text_xs()
.child(
v_flex()
.gap_1p5()
.w_full()
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Public Key:")),
@@ -166,7 +165,7 @@ impl Render for BackupPanel {
.w_full()
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child(SharedString::from("Secret Key:")),
+21 -28
View File
@@ -1,11 +1,3 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, WindowExt, h_flex, v_flex};
use std::ops::Range;
use std::rc::Rc;
@@ -17,11 +9,18 @@ use gpui::{
size, transparent_black,
};
use gpui_base::{VirtualListScrollHandle, v_virtual_list};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, WindowExt, h_flex, v_flex};
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry, Membership};
use nostr_sdk::prelude::{RelayUrl, Timestamp};
use settings::AppSettings;
use smallvec::{SmallVec, smallvec};
use ui::{TextStyle, Typeset as _, button_radius, panel_radius};
use ui::Avatar;
const TITLE: &str = "Browse";
const INPUT_PLACEHOLDER: &str = "wss://relay.example.com";
@@ -269,7 +268,7 @@ impl BrowsePanel {
.min_w_0()
.child(
div()
.text_style(TextStyle::Callout)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.line_height(relative(1.2))
.child("Public Groups"),
@@ -277,7 +276,7 @@ impl BrowsePanel {
.child(
div()
.truncate()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.map(|this| match &self.relay {
Some(relay) => {
@@ -295,7 +294,7 @@ impl BrowsePanel {
.w_full()
.max_w_64()
.px_1()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.bg(cx.theme().sidebar)
.child(Input::new(&self.input).appearance(false).small().flex_1())
.child(
@@ -372,7 +371,7 @@ impl BrowsePanel {
.min_w_0()
.h_full()
.overflow_hidden()
.rounded(px(panel_radius()))
.rounded(cx.theme().radius_lg)
.cursor_pointer()
.border_1()
.border_color(transparent_black())
@@ -386,20 +385,14 @@ impl BrowsePanel {
.flex_1()
.min_h_0()
.p_2()
.gap(px(ui::SPACE))
.gap_2()
.child(
h_flex()
.w_full()
.items_center()
.gap(px(ui::SPACE))
.gap_2()
.when(!hide_avatar, |this| {
this.child(match picture {
Some(picture) => Avatar::new()
.src(picture)
.flex_shrink_0()
.into_any_element(),
None => Avatar::new().flex_shrink_0().into_any_element(),
})
this.child(Avatar::new(picture).seed(seed).flex_shrink_0())
})
.child(
v_flex()
@@ -407,7 +400,7 @@ impl BrowsePanel {
.min_w_0()
.child(
div()
.text_style(TextStyle::Callout)
.text_xs()
.truncate()
.font_weight(gpui::FontWeight::SEMIBOLD)
.line_height(relative(1.2))
@@ -416,7 +409,7 @@ impl BrowsePanel {
.child(
div()
.truncate()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(relay),
),
@@ -425,7 +418,7 @@ impl BrowsePanel {
.child(
div()
.min_w_0()
.text_style(TextStyle::Callout)
.text_xs()
.text_color(cx.theme().muted_foreground)
.text_ellipsis()
.line_clamp(2)
@@ -446,7 +439,7 @@ impl BrowsePanel {
.child(Icon::new(IconName::Book))
.child(
div()
.text_style(TextStyle::Callout)
.text_xs()
.child(SharedString::from(format!("{members} members"))),
),
),
@@ -468,7 +461,7 @@ impl Panel for BrowsePanel {
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_1()
.text_style(TextStyle::Caption)
.text_xs()
.child(
Icon::new(IconName::Compass)
.small()
@@ -548,7 +541,7 @@ fn notice(text: &'static str, cx: &App) -> AnyElement {
h_flex()
.size_full()
.justify_center()
.text_style(TextStyle::Callout)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(text)
.into_any_element()
+22 -24
View File
@@ -1,10 +1,3 @@
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::scroll::ScrollableElement;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use std::collections::HashSet;
use anyhow::Error;
@@ -12,14 +5,20 @@ use gpui::prelude::FluentBuilder;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task, TextAlign,
Window, div, px, rems, retain_all,
Window, div, rems, retain_all,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::input::{Input, InputEvent, InputState};
use gpui_kit::component::scroll::ScrollableElement;
use gpui_kit::component::{ActiveTheme, Disableable, Sizable, WindowExt, h_flex, v_flex};
use instant::Duration;
use nostr_sdk::prelude::*;
use person::Directory;
use smallvec::{SmallVec, smallvec};
use state::NostrRegistry;
use ui::{TextStyle, Typeset as _, button_radius, panel_radius};
use ui::Avatar;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ContactListPanel> {
cx.new(|cx| ContactListPanel::new(window, cx))
@@ -242,18 +241,17 @@ impl ContactListPanel {
.h_8()
.px_2()
.justify_between()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.hover(|this| this.bg(cx.theme().primary))
.child(
h_flex()
.gap(px(ui::SPACE))
.text_style(TextStyle::Callout)
.child(match profile.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => Avatar::new().small().into_any_element(),
})
.gap_2()
.text_xs()
.child(
Avatar::new(profile.avatar())
.seed(profile.avatar_seed())
.small(),
)
.child(profile.name()),
)
.child(
@@ -283,8 +281,8 @@ impl ContactListPanel {
.border_2()
.border_dashed()
.border_color(cx.theme().border)
.rounded(px(panel_radius()))
.text_style(TextStyle::Callout)
.rounded(cx.theme().radius_lg)
.text_xs()
.text_align(TextAlign::Center)
.child(SharedString::from("Please add some relays."))
}
@@ -320,13 +318,13 @@ impl Render for ContactListPanel {
.overflow_y_scrollbar()
.child(
v_flex()
.gap(px(ui::SPACE))
.gap_2()
.flex_1()
.w_full()
.text_style(TextStyle::Callout)
.text_xs()
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child("New contact:"),
@@ -354,7 +352,7 @@ impl Render for ContactListPanel {
this.child(
div()
.italic()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().danger)
.child(error.clone()),
)
+30 -46
View File
@@ -1,12 +1,3 @@
use crate::dock::add_panel_to;
use crate::nav::Nav;
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
use gpui_kit::component::scroll::ScrollableElement;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use std::time::Duration;
use anyhow::Error;
@@ -16,15 +7,23 @@ use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, px, svg,
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, svg,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
use gpui_kit::component::group_box::{GroupBox, GroupBoxVariants};
use gpui_kit::component::scroll::ScrollableElement;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{SmallVec, smallvec};
use state::{NostrRegistry, StateEvent};
use ui::{TextStyle, Typeset as _, button_radius};
use ui::Avatar;
use super::browse;
use crate::dock::add_panel_to;
use crate::nav::Nav;
use crate::sidebar::open_room;
/// How many requests and follow ups to show per section.
@@ -205,8 +204,8 @@ impl GreeterPanel {
.border_1()
.border_dashed()
.border_color(cx.theme().border)
.rounded(px(button_radius()))
.text_style(TextStyle::Caption)
.rounded(cx.theme().radius)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("No chat requests")
.into_any_element(),
@@ -219,9 +218,6 @@ impl GreeterPanel {
let room = room.clone();
let dock = dock.clone();
let name = room.read(cx).display_name(cx);
let picture = room.read(cx).display_image(cx);
#[allow(unused_variables)]
#[allow(unused_variables)]
let seed = room.read(cx).display_image_seed(cx);
let created_at = room.read(cx).created_at.to_ago();
let preview = preview.clone();
@@ -233,7 +229,7 @@ impl GreeterPanel {
.h_9()
.px_1p5()
.gap_2()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.cursor_pointer()
.hover(|this| this.bg(cx.theme().primary))
.on_click({
@@ -243,23 +239,18 @@ impl GreeterPanel {
open_room(&room, &dock, window, cx);
}
})
.child(match picture {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().flex_shrink_0().into_any_element(),
})
.child(
div()
.min_w_0()
.truncate()
.text_style(TextStyle::Callout)
.child(name),
Avatar::new(room.read(cx).display_image(cx))
.seed(seed)
.small(),
)
.child(div().min_w_0().truncate().text_xs().child(name))
.child(
div()
.flex_1()
.min_w_0()
.truncate()
.text_style(TextStyle::Callout)
.text_xs()
.text_color(cx.theme().muted_foreground)
.italic()
.child(preview),
@@ -283,7 +274,7 @@ impl GreeterPanel {
.child(
div()
.text_align(gpui::TextAlign::Right)
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(created_at),
),
@@ -333,8 +324,6 @@ impl GreeterPanel {
let room = room.clone();
let dock = dock.clone();
let name = room.read(cx).display_name(cx);
let picture = room.read(cx).display_image(cx);
#[allow(unused_variables)]
let seed = room.read(cx).display_image_seed(cx);
let created_at = room.read(cx).created_at.to_ago();
let preview = preview.clone();
@@ -346,7 +335,7 @@ impl GreeterPanel {
.h_9()
.px_1p5()
.gap_2()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.cursor_pointer()
.hover(|this| this.bg(cx.theme().primary))
.on_click({
@@ -356,23 +345,18 @@ impl GreeterPanel {
open_room(&room, &dock, window, cx);
}
})
.child(match picture {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().flex_shrink_0().into_any_element(),
})
.child(
div()
.min_w_0()
.truncate()
.text_style(TextStyle::Callout)
.child(name),
Avatar::new(room.read(cx).display_image(cx))
.seed(seed)
.small(),
)
.child(div().min_w_0().truncate().text_xs().child(name))
.child(
div()
.flex_1()
.min_w_0()
.truncate()
.text_style(TextStyle::Callout)
.text_xs()
.text_color(cx.theme().muted_foreground)
.italic()
.child(preview),
@@ -396,7 +380,7 @@ impl GreeterPanel {
.child(
div()
.text_align(gpui::TextAlign::Right)
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(created_at),
),
@@ -439,7 +423,7 @@ impl GreeterPanel {
.child(
div()
.text_color(cx.theme().muted_foreground)
.text_style(TextStyle::Subheadline)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.child("Find new friends"),
)
@@ -454,7 +438,7 @@ impl GreeterPanel {
.text_color(cx.theme().muted_foreground),
)
.label("Browse Public Groups")
.text_style(TextStyle::Callout)
.text_xs()
.on_click(cx.listener(|this, _, window, cx| {
let panel = browse::init(this.dock.clone(), window, cx);
add_panel_to(
@@ -470,7 +454,7 @@ impl GreeterPanel {
Nav::new("browse-posts")
.prefix(Icon::new(IconName::Link).small())
.label("Browse Nostr via Jumble")
.text_style(TextStyle::Callout)
.text_xs()
.on_click(|_, _, cx| {
cx.open_url(JUMBLE_URL);
}),
@@ -489,7 +473,7 @@ impl Panel for GreeterPanel {
fn title(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_1()
.text_style(TextStyle::Caption)
.text_xs()
.child(
svg()
.path("brand/coop.svg")
+1 -2
View File
@@ -5,7 +5,6 @@ use gpui::{
use gpui_kit::assets::IconName;
use gpui_kit::component::dock::{Panel, PanelEvent};
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex};
use ui::{TextStyle, Typeset as _};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<InboxPanel> {
cx.new(|cx| InboxPanel::new(window, cx))
@@ -58,7 +57,7 @@ impl Render for InboxPanel {
h_flex()
.size_full()
.justify_center()
.text_style(TextStyle::Callout)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(self.name.clone())
}
+17 -20
View File
@@ -4,10 +4,10 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, ClipboardItem, Context, Entity, InteractiveElement as _, IntoElement,
ParentElement, SharedString, Styled, WeakEntity, div, px, relative, uniform_list,
ParentElement, SharedString, Styled, WeakEntity, div, relative, uniform_list,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
@@ -15,7 +15,7 @@ use nip29::Group;
use nostr_sdk::prelude::*;
use settings::AppSettings;
use state::NostrRegistry;
use ui::{TextStyle, Typeset as _};
use ui::Avatar;
use super::Sidebar;
use crate::dialogs::profile;
@@ -87,7 +87,7 @@ impl Sidebar {
.current_user()
.is_some_and(|me| group.read(cx).admins().contains(&me));
let (name, _seed, picture, total_members, rows) = {
let (name, seed, picture, total_members, rows) = {
let group = group.read(cx);
let admins = group.admins();
@@ -144,7 +144,7 @@ impl Sidebar {
.flex_1()
.min_h_0()
.w_full()
.gap(px(ui::SPACE))
.gap_2()
.child(
h_flex()
.px_2()
@@ -155,19 +155,14 @@ impl Sidebar {
.pl_0p5()
.gap_1()
.flex_shrink_1()
.child(match picture {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => Avatar::new().small().into_any_element(),
})
.child(Avatar::new(picture).seed(seed.clone()).small())
.child(
v_flex()
.flex_1()
.min_w_0()
.child(
div()
.text_style(TextStyle::Caption)
.text_xs()
.truncate()
.line_height(relative(1.2))
.font_weight(gpui::FontWeight::SEMIBOLD)
@@ -175,7 +170,8 @@ impl Sidebar {
)
.child(
div()
.text_style(TextStyle::Caption2)
.text_xs()
.font_medium()
.text_color(cx.theme().muted_foreground)
.child(total_members),
),
@@ -253,7 +249,7 @@ impl Sidebar {
.small()
.text_color(cx.theme().muted_foreground),
)
.text_style(TextStyle::Caption)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.on_click(cx.listener(move |this, _event, _window, cx| {
@@ -289,20 +285,21 @@ impl Sidebar {
let nav = Nav::new(public_key.to_hex())
.label(person.name())
.text_style(TextStyle::Callout)
.text_xs()
.font_weight(gpui::FontWeight::MEDIUM)
.clickable(true)
.when(!hide_avatar, |this| {
this.prefix(match person.avatar() {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().flex_shrink_0().into_any_element(),
})
this.prefix(
Avatar::new(person.avatar())
.seed(person.avatar_seed())
.small(),
)
})
.when(!roles.is_empty(), |this| {
this.suffix(
div()
.truncate()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(SharedString::from(roles.join(", "))),
)
+11 -13
View File
@@ -5,16 +5,15 @@ use gpui::{
Stateful, Styled, div, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::button::{Button, ButtonVariants};
use gpui_kit::component::menu::DropdownMenu;
use gpui_kit::component::{Disableable, Selectable, Sizable, h_flex};
use nostr_sdk::prelude::*;
use ui::TABBAR_HEIGHT;
use ui::TRAFFIC_LIGHT_PADDING;
use ui::Avatar;
use super::Sidebar;
use crate::Command;
use crate::dock::TAB_BAR_HEIGHT;
impl Sidebar {
pub(super) fn render_user(
@@ -29,20 +28,19 @@ impl Sidebar {
h_flex()
.id("sidebar-user")
.w_full()
.h(TABBAR_HEIGHT)
.h(TAB_BAR_HEIGHT)
.flex_shrink_0()
.items_center()
.gap(px(ui::SPACE))
.gap_2()
.px_2()
.when(cfg!(target_os = "macos"), |this| {
this.pl(px(TRAFFIC_LIGHT_PADDING))
})
.when(cfg!(target_os = "macos"), |this| this.pl(px(80.)))
.child(
Button::new("current-user")
.child(match avatar.clone() {
Some(picture) => Avatar::new().src(picture).small().into_any_element(),
None => Avatar::new().small().into_any_element(),
})
.child(
Avatar::new(avatar.clone())
.seed(profile.avatar_seed())
.small(),
)
.small()
.dropdown_caret(true)
.compact()
@@ -127,7 +125,7 @@ impl Sidebar {
let staged = updater.read(cx).staged();
h_flex()
.gap(px(ui::SPACE))
.gap_2()
.when(!up_to_date, |this| {
this.child(
Button::new("update")
+1 -2
View File
@@ -10,7 +10,6 @@ use gpui_kit::component::notification::Notification;
use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
use nostr_sdk::prelude::*;
use state::NostrRegistry;
use ui::{TextStyle, Typeset as _};
use super::Sidebar;
use super::rows::SidebarRow;
@@ -211,7 +210,7 @@ impl Sidebar {
.child(
h_flex()
.gap_1()
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child("Selected")
.child(
+11 -11
View File
@@ -1,22 +1,21 @@
use crate::nav::Nav;
use gpui_kit::assets::IconName;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use std::rc::Rc;
use chat::{ChatRegistry, RoomKind};
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled, div, px,
uniform_list,
AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled, div, uniform_list,
};
use gpui_kit::assets::IconName;
use gpui_kit::base::StyledExt as _;
use gpui_kit::component::scroll::Scrollbar;
use gpui_kit::component::{ActiveTheme, Icon, Sizable, h_flex, v_flex};
use nip29::GroupsRegistry;
use settings::AppSettings;
use ui::{TextStyle, Typeset as _, control_radius};
use super::Sidebar;
use super::rows::SidebarRow;
use crate::Command;
use crate::nav::Nav;
/// A collapsible section of the sidebar's main list.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -62,7 +61,7 @@ impl Sidebar {
.flex_1()
.min_h_0()
.relative()
.gap(px(ui::SPACE))
.gap_2()
.child(
v_flex()
.px_2()
@@ -90,10 +89,11 @@ impl Sidebar {
.px_1()
.min_w_6()
.justify_center()
.text_style(TextStyle::Caption2)
.text_xs()
.font_medium()
.text_color(cx.theme().muted_foreground)
.text_center()
.rounded(px(control_radius()))
.rounded(cx.theme().radius)
.bg(cx.theme().secondary)
.child(SharedString::from(
request_count.to_string(),
@@ -128,7 +128,7 @@ impl Sidebar {
this.child(
div()
.px_4()
.text_style(TextStyle::Caption)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child(list_title),
+8 -6
View File
@@ -7,7 +7,7 @@ use gpui::prelude::FluentBuilder;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled, StyledImage, Subscription,
Task, UniformListScrollHandle, WeakEntity, Window, div, img, px, retain_all,
Task, UniformListScrollHandle, WeakEntity, Window, div, img, retain_all,
};
use gpui_kit::component::dock::{DockArea, Panel, PanelEvent};
use gpui_kit::component::input::{InputEvent, InputState};
@@ -20,7 +20,9 @@ use nostr_sdk::prelude::*;
use person::{Directory, PersonRegistry};
use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry, StateEvent};
use ui::{TABBAR_HEIGHT, TextStyle, Typeset as _, title_bar_drag_handlers};
use ui::title_bar_drag_handlers;
use crate::dock::TAB_BAR_HEIGHT;
mod group_view;
mod header;
@@ -307,7 +309,7 @@ impl Render for Sidebar {
.image_cache(retain_all("sidebar"))
.size_full()
.relative()
.gap(px(ui::SPACE))
.gap_2()
.bg(cx.theme().sidebar)
.when_some(current_user.as_ref(), |this, current_user| {
this.child(title_bar_drag_handlers(
@@ -325,7 +327,7 @@ impl Render for Sidebar {
.absolute()
.top_0()
.left_0()
.h(TABBAR_HEIGHT)
.h(TAB_BAR_HEIGHT)
.w_full(),
window,
cx,
@@ -357,14 +359,14 @@ impl Render for Sidebar {
.px_8()
.child(
h_flex()
.gap(px(ui::SPACE))
.gap_2()
.w_full()
.h_9()
.justify_center()
.bg(cx.theme().background.opacity(0.85))
.when(cx.theme().shadow, |this| this.shadow_md())
.rounded_full()
.text_style(TextStyle::Caption)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.child(Spinner::new().small().color(cx.theme().primary))
+39 -58
View File
@@ -8,7 +8,6 @@ use gpui::{
SharedString, Styled, WeakEntity, Window, div, px,
};
use gpui_kit::assets::IconName;
use gpui_kit::component::avatar::Avatar;
use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
@@ -16,7 +15,7 @@ use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_f
use nip29::Group;
use nostr_sdk::prelude::*;
use settings::AppSettings;
use ui::{TextStyle, Typeset as _, button_radius};
use ui::Avatar;
use super::Sidebar;
use super::list::ListSection;
@@ -100,7 +99,7 @@ impl Sidebar {
.small()
.text_color(cx.theme().muted_foreground),
)
.text_style(TextStyle::Caption)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.on_click(cx.listener(move |this, _event, _window, cx| {
@@ -122,7 +121,7 @@ impl Sidebar {
Nav::new(label)
.label(label)
.text_style(TextStyle::Caption)
.text_xs()
.font_weight(gpui::FontWeight::SEMIBOLD)
.text_color(cx.theme().muted_foreground)
.into_any_element()
@@ -140,8 +139,8 @@ impl Sidebar {
.border_1()
.border_dashed()
.border_color(cx.theme().border)
.rounded(px(button_radius()))
.text_style(TextStyle::Caption)
.rounded(cx.theme().radius)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(*text),
)
@@ -149,8 +148,6 @@ impl Sidebar {
SidebarRow::Room { room } => {
let id = room.read(cx).id;
let name = room.read(cx).display_name(cx);
let picture = room.read(cx).display_image(cx);
#[allow(unused_variables)]
let seed = room.read(cx).display_image_seed(cx);
let created_at = room.read(cx).created_at.to_ago();
let member = room.read(cx).members().first().copied();
@@ -163,16 +160,13 @@ impl Sidebar {
let nav = Nav::new(index)
.label(name)
.text_style(TextStyle::Callout)
.text_xs()
.when(!hide_avatar, |this| {
this.prefix(match picture {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
this.prefix(
Avatar::new(room.read(cx).display_image(cx))
.seed(seed)
.small(),
)
})
.suffix(
h_flex()
@@ -181,7 +175,7 @@ impl Sidebar {
this.child(
div()
.px_1()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.bg(cx.theme().danger.opacity(0.15))
.text_size(px(8.0))
.text_color(cx.theme().danger)
@@ -192,7 +186,7 @@ impl Sidebar {
this.child(
div()
.font_weight(gpui::FontWeight::NORMAL)
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(created_at),
)
@@ -236,16 +230,13 @@ impl Sidebar {
Nav::new(index)
.label(person.name())
.text_style(TextStyle::Callout)
.text_xs()
.when(!hide_avatar, |this| {
this.prefix(match person.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
this.prefix(
Avatar::new(person.avatar())
.seed(person.avatar_seed())
.small(),
)
})
.selected(selected)
.on_click(move |_event, _window, cx| {
@@ -263,16 +254,13 @@ impl Sidebar {
Nav::new(index)
.label(person.name())
.text_style(TextStyle::Callout)
.text_xs()
.when(!hide_avatar, |this| {
this.prefix(match person.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
this.prefix(
Avatar::new(person.avatar())
.seed(person.avatar_seed())
.small(),
)
})
.on_click(move |_event, window, cx| {
profile::open(public_key, window, cx);
@@ -285,7 +273,6 @@ impl Sidebar {
let group = group.clone();
let name = group.read(cx).display_name();
let picture = group.read(cx).display_image();
let seed = group.read(cx).key().cache_tag();
let tag = seed.clone();
let pinned = AppSettings::global(cx).read(cx).pinned_group(&tag, cx);
@@ -300,22 +287,19 @@ impl Sidebar {
let nav = Nav::new(index)
.label(name)
.text_style(TextStyle::Callout)
.text_xs()
.when(!hide_avatar, |this| {
this.prefix(match picture {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
this.prefix(
Avatar::new(group.read(cx).display_image())
.seed(seed)
.small(),
)
})
.when_some(suffix, |this, suffix| {
this.suffix(
div()
.font_weight(gpui::FontWeight::NORMAL)
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(suffix),
)
@@ -376,22 +360,19 @@ impl Sidebar {
Nav::new(index)
.label(content)
.text_style(TextStyle::Callout)
.text_xs()
.font_weight(gpui::FontWeight::MEDIUM)
.when(!hide_avatar, |this| {
this.prefix(match person.avatar() {
Some(picture) => {
Avatar::new().src(picture).small().into_any_element()
}
None => {
Avatar::new().small().flex_shrink_0().into_any_element()
}
})
this.prefix(
Avatar::new(person.avatar())
.seed(person.avatar_seed())
.small(),
)
})
.suffix(
div()
.font_weight(gpui::FontWeight::NORMAL)
.text_style(TextStyle::Caption)
.text_xs()
.text_color(cx.theme().muted_foreground)
.child(detail),
)
+2 -3
View File
@@ -9,12 +9,11 @@ use std::rc::Rc;
use chat::{ChatRegistry, Room};
use gpui::{
AnyElement, App, Context, Entity, IntoElement, ParentElement, SharedString, Styled, Task,
Window, div, px, uniform_list,
Window, div, uniform_list,
};
use nip29::{Group, GroupsRegistry};
use nostr_sdk::prelude::*;
use state::{FIND_LIMIT, NostrRegistry};
use ui::button_radius;
use super::Sidebar;
use super::rows::SidebarRow;
@@ -215,7 +214,7 @@ impl Sidebar {
.px_1p5()
.gap_1()
.w_full()
.rounded(px(button_radius()))
.rounded(cx.theme().radius)
.bg(cx.theme().secondary)
.child(Input::new(&self.find_input).appearance(false).small())
.child(