Redesign for the v1 stable release #3
@@ -5,12 +5,12 @@ use gpui::{
|
|||||||
div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||||
};
|
};
|
||||||
use state::NostrRegistry;
|
use state::{NostrRegistry, RelayState};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::panels::{connect, import, profile};
|
use crate::panels::{connect, import, profile, relay_list};
|
||||||
use crate::workspace::Workspace;
|
use crate::workspace::Workspace;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
||||||
@@ -71,6 +71,11 @@ impl Render for GreeterPanel {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let identity = nostr.read(cx).identity();
|
let identity = nostr.read(cx).identity();
|
||||||
|
|
||||||
|
let relay_list_state = identity.read(cx).relay_list_state();
|
||||||
|
let messaging_relay_state = identity.read(cx).messaging_relays_state();
|
||||||
|
let required_actions =
|
||||||
|
relay_list_state == RelayState::NotSet || messaging_relay_state == RelayState::NotSet;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
@@ -78,15 +83,16 @@ impl Render for GreeterPanel {
|
|||||||
.p_2()
|
.p_2()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_3()
|
|
||||||
.h_full()
|
.h_full()
|
||||||
|
.w_112()
|
||||||
|
.gap_6()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.mb_7()
|
.mb_4()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.w_96()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
svg()
|
svg()
|
||||||
.path("brand/coop.svg")
|
.path("brand/coop.svg")
|
||||||
@@ -110,11 +116,74 @@ impl Render for GreeterPanel {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.when(required_actions, |this| {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.w_full()
|
||||||
|
.items_start()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from("Required Actions"))
|
||||||
|
.child(div().flex_1().h_px().bg(cx.theme().border)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.items_start()
|
||||||
|
.justify_start()
|
||||||
|
.gap_2()
|
||||||
|
.when(relay_list_state == RelayState::NotSet, |this| {
|
||||||
|
this.child(
|
||||||
|
Button::new("connect")
|
||||||
|
.icon(Icon::new(IconName::Door))
|
||||||
|
.label("Set up relay list")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.on_click(move |_ev, window, cx| {
|
||||||
|
Workspace::add_panel(
|
||||||
|
relay_list::init(window, cx),
|
||||||
|
DockPlacement::Center,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(
|
||||||
|
messaging_relay_state == RelayState::NotSet,
|
||||||
|
|this| {
|
||||||
|
this.child(
|
||||||
|
Button::new("import")
|
||||||
|
.icon(Icon::new(IconName::Usb))
|
||||||
|
.label("Set up messaging relays")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.on_click(move |_ev, window, cx| {
|
||||||
|
Workspace::add_panel(
|
||||||
|
import::init(window, cx),
|
||||||
|
DockPlacement::Center,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
.when(!identity.read(cx).owned, |this| {
|
.when(!identity.read(cx).owned, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.w_96()
|
.w_full()
|
||||||
|
.items_start()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
@@ -167,7 +236,8 @@ impl Render for GreeterPanel {
|
|||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.w_96()
|
.w_full()
|
||||||
|
.items_start()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ pub mod connect;
|
|||||||
pub mod greeter;
|
pub mod greeter;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
pub mod relay_list;
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ impl Render for ProfilePanel {
|
|||||||
.child(divider(cx))
|
.child(divider(cx))
|
||||||
.child(
|
.child(
|
||||||
Button::new("submit")
|
Button::new("submit")
|
||||||
.label("Continue")
|
.label("Update")
|
||||||
.primary()
|
.primary()
|
||||||
.disabled(self.uploading)
|
.disabled(self.uploading)
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
|||||||
366
crates/coop/src/panels/relay_list.rs
Normal file
366
crates/coop/src/panels/relay_list.rs
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
|
use common::BOOTSTRAP_RELAYS;
|
||||||
|
use dock::panel::{Panel, PanelEvent};
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
|
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||||
|
Styled, Subscription, Task, TextAlign, UniformList, Window,
|
||||||
|
};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::NostrRegistry;
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::{divider, h_flex, v_flex, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<RelayListPanel> {
|
||||||
|
cx.new(|cx| RelayListPanel::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RelayListPanel {
|
||||||
|
name: SharedString,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
|
||||||
|
/// Relay URL input
|
||||||
|
input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Relay metadata input
|
||||||
|
metadata: Entity<Option<RelayMetadata>>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Option<SharedString>,
|
||||||
|
|
||||||
|
// All relays
|
||||||
|
relays: HashSet<(RelayUrl, Option<RelayMetadata>)>,
|
||||||
|
|
||||||
|
// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||||
|
|
||||||
|
// Background tasks
|
||||||
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RelayListPanel {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
|
||||||
|
let metadata = cx.new(|_| None);
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
|
let mut subscriptions = smallvec![];
|
||||||
|
let mut tasks = smallvec![];
|
||||||
|
|
||||||
|
tasks.push(
|
||||||
|
// Load user's relays in the local database
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = cx
|
||||||
|
.background_spawn(async move { Self::load(&client).await })
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(relays) = result {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.relays.extend(relays);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to user's input events
|
||||||
|
cx.subscribe_in(&input, window, move |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.add(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
name: "Update Relay List".into(),
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
input,
|
||||||
|
metadata,
|
||||||
|
relays: HashSet::new(),
|
||||||
|
error: None,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
_tasks: tasks,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load(client: &Client) -> Result<Vec<(RelayUrl, Option<RelayMetadata>)>, Error> {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||||
|
Ok(nip65::extract_owned_relay_list(event).collect())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Not found."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let value = self.input.read(cx).value().to_string();
|
||||||
|
let metadata = self.metadata.read(cx);
|
||||||
|
|
||||||
|
if !value.starts_with("ws") {
|
||||||
|
self.set_error("Relay URl is invalid", window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(url) = RelayUrl::parse(&value) {
|
||||||
|
if !self.relays.insert((url, metadata.to_owned())) {
|
||||||
|
self.input.update(cx, |this, cx| {
|
||||||
|
this.set_value("", window, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.set_error("Relay URl is invalid", window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, url: &RelayUrl, cx: &mut Context<Self>) {
|
||||||
|
self.relays.retain(|(relay, _)| relay != url);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<E>(&mut self, error: E, window: &mut Window, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
E: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error = Some(error.into());
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
|
// Clear the error message after a delay
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error = None;
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_relays(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.relays.is_empty() {
|
||||||
|
self.set_error("You need to add at least 1 relay", window, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
let relays = self.relays.clone();
|
||||||
|
|
||||||
|
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let event = EventBuilder::relay_list(relays).sign(&signer).await?;
|
||||||
|
|
||||||
|
// Set relay list for current user
|
||||||
|
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(_) => {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_error(e.to_string(), window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> UniformList {
|
||||||
|
let relays = self.relays.clone();
|
||||||
|
let total = relays.len();
|
||||||
|
|
||||||
|
uniform_list(
|
||||||
|
"relays",
|
||||||
|
total,
|
||||||
|
cx.processor(move |_v, range, _window, cx| {
|
||||||
|
let mut items = Vec::new();
|
||||||
|
|
||||||
|
for ix in range {
|
||||||
|
let Some((url, metadata)) = relays.iter().nth(ix) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
div()
|
||||||
|
.id(SharedString::from(url.to_string()))
|
||||||
|
.group("")
|
||||||
|
.w_full()
|
||||||
|
.h_9()
|
||||||
|
.py_0p5()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.px_2()
|
||||||
|
.flex()
|
||||||
|
.justify_between()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.child(
|
||||||
|
div().text_sm().child(SharedString::from(url.to_string())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.text_xs()
|
||||||
|
.map(|this| {
|
||||||
|
if let Some(metadata) = metadata {
|
||||||
|
this.child(SharedString::from(
|
||||||
|
metadata.to_string(),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
this.child(SharedString::from("Read+Write"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Button::new("remove_{ix}")
|
||||||
|
.icon(IconName::Close)
|
||||||
|
.xsmall()
|
||||||
|
.ghost()
|
||||||
|
.invisible()
|
||||||
|
.group_hover("", |this| this.visible())
|
||||||
|
.on_click({
|
||||||
|
let url = url.to_owned();
|
||||||
|
cx.listener(
|
||||||
|
move |this, _ev, _window, cx| {
|
||||||
|
this.remove(&url, cx);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.h_full()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_empty(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.mt_2()
|
||||||
|
.h_20()
|
||||||
|
.justify_center()
|
||||||
|
.border_2()
|
||||||
|
.border_dashed()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.rounded(cx.theme().radius_lg)
|
||||||
|
.text_sm()
|
||||||
|
.text_align(TextAlign::Center)
|
||||||
|
.child(SharedString::from("Please add some relays."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel for RelayListPanel {
|
||||||
|
fn panel_id(&self) -> SharedString {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self, _cx: &App) -> AnyElement {
|
||||||
|
self.name.clone().into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<PanelEvent> for RelayListPanel {}
|
||||||
|
|
||||||
|
impl Focusable for RelayListPanel {
|
||||||
|
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for RelayListPanel {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.p_2()
|
||||||
|
.gap_10()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_center()
|
||||||
|
.font_semibold()
|
||||||
|
.line_height(relative(1.25))
|
||||||
|
.child(SharedString::from("Update Relay List")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_112()
|
||||||
|
.gap_2()
|
||||||
|
.text_sm()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.child(TextInput::new(&self.input).small())
|
||||||
|
.child(
|
||||||
|
Button::new("add")
|
||||||
|
.icon(IconName::Plus)
|
||||||
|
.label("Add")
|
||||||
|
.ghost()
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.add(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.error.as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().danger_foreground)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.map(|this| {
|
||||||
|
if !self.relays.is_empty() {
|
||||||
|
this.child(self.render_list(window, cx))
|
||||||
|
} else {
|
||||||
|
this.child(self.render_empty(window, cx))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(divider(cx))
|
||||||
|
.child(
|
||||||
|
Button::new("submit")
|
||||||
|
.label("Update")
|
||||||
|
.primary()
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.set_relays(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEA
|
|||||||
use dock::panel::{Panel, PanelEvent};
|
use dock::panel::{Panel, PanelEvent};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, rems, uniform_list, App, AppContext, Context, Decorations, Entity, EventEmitter,
|
deferred, div, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
Focusable, InteractiveElement, IntoElement, ParentElement, Render, RetainAllImageCache,
|
||||||
RetainAllImageCache, SharedString, Styled, Subscription, Task, Window,
|
SharedString, Styled, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use list_item::RoomListItem;
|
use list_item::RoomListItem;
|
||||||
@@ -17,7 +17,7 @@ use nostr_sdk::prelude::*;
|
|||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION};
|
use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION};
|
||||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT};
|
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
@@ -578,9 +578,7 @@ impl Focusable for Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
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 {
|
||||||
let decorations = window.window_decorations();
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let identity = nostr.read(cx).identity();
|
let identity = nostr.read(cx).identity();
|
||||||
|
|
||||||
@@ -601,12 +599,6 @@ impl Render for Sidebar {
|
|||||||
.relative()
|
.relative()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.bg(cx.theme().surface_background)
|
.bg(cx.theme().surface_background)
|
||||||
.map(|this| match decorations {
|
|
||||||
Decorations::Server => this,
|
|
||||||
Decorations::Client { .. } => this
|
|
||||||
.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING),
|
|
||||||
})
|
|
||||||
// Titlebar
|
// Titlebar
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use crate::tab_panel::TabPanel;
|
|||||||
|
|
||||||
pub mod dock;
|
pub mod dock;
|
||||||
pub mod panel;
|
pub mod panel;
|
||||||
mod platforms;
|
|
||||||
pub mod resizable;
|
pub mod resizable;
|
||||||
pub mod stack_panel;
|
pub mod stack_panel;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
svg, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
|
||||||
StatefulInteractiveElement, Styled, Window,
|
|
||||||
};
|
|
||||||
use linicon::{lookup_icon, IconType};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::{h_flex, Icon, IconName, Sizable};
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct LinuxWindowControls {}
|
|
||||||
|
|
||||||
impl LinuxWindowControls {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for LinuxWindowControls {
|
|
||||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.id("linux-window-controls")
|
|
||||||
.gap_2()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
|
||||||
.child(WindowControl::new(
|
|
||||||
LinuxControl::Minimize,
|
|
||||||
IconName::WindowMinimize,
|
|
||||||
))
|
|
||||||
.child({
|
|
||||||
if window.is_maximized() {
|
|
||||||
WindowControl::new(LinuxControl::Restore, IconName::WindowRestore)
|
|
||||||
} else {
|
|
||||||
WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(WindowControl::new(
|
|
||||||
LinuxControl::Close,
|
|
||||||
IconName::WindowClose,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct WindowControl {
|
|
||||||
kind: LinuxControl,
|
|
||||||
fallback: IconName,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowControl {
|
|
||||||
pub fn new(kind: LinuxControl, fallback: IconName) -> Self {
|
|
||||||
Self { kind, fallback }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn is_gnome(&self) -> bool {
|
|
||||||
matches!(detect_desktop_environment(), DesktopEnvironment::Gnome)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for WindowControl {
|
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.id(self.kind.as_icon_name())
|
|
||||||
.group("")
|
|
||||||
.justify_center()
|
|
||||||
.items_center()
|
|
||||||
.rounded_full()
|
|
||||||
.size_6()
|
|
||||||
.map(|this| {
|
|
||||||
if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() {
|
|
||||||
this.child(
|
|
||||||
svg()
|
|
||||||
.external_path(path.into_os_string().into_string().unwrap())
|
|
||||||
.text_color(cx.theme().text)
|
|
||||||
.size_4(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(Icon::new(self.fallback).flex_grow().small())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_mouse_move(|_ev, _window, cx| cx.stop_propagation())
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
match self.kind {
|
|
||||||
LinuxControl::Minimize => window.minimize_window(),
|
|
||||||
LinuxControl::Restore => window.zoom_window(),
|
|
||||||
LinuxControl::Maximize => window.zoom_window(),
|
|
||||||
LinuxControl::Close => cx.quit(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static DE: OnceLock<DesktopEnvironment> = OnceLock::new();
|
|
||||||
static LINUX_CONTROLS: OnceLock<HashMap<LinuxControl, Option<PathBuf>>> = OnceLock::new();
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum DesktopEnvironment {
|
|
||||||
Gnome,
|
|
||||||
Kde,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Detect the current desktop environment
|
|
||||||
pub fn detect_desktop_environment() -> &'static DesktopEnvironment {
|
|
||||||
DE.get_or_init(|| {
|
|
||||||
// Try to use environment variables first
|
|
||||||
if let Ok(output) = std::env::var("XDG_CURRENT_DESKTOP") {
|
|
||||||
let desktop = output.to_lowercase();
|
|
||||||
if desktop.contains("gnome") {
|
|
||||||
return DesktopEnvironment::Gnome;
|
|
||||||
} else if desktop.contains("kde") {
|
|
||||||
return DesktopEnvironment::Kde;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback detection methods
|
|
||||||
if let Ok(output) = std::env::var("DESKTOP_SESSION") {
|
|
||||||
let session = output.to_lowercase();
|
|
||||||
if session.contains("gnome") {
|
|
||||||
return DesktopEnvironment::Gnome;
|
|
||||||
} else if session.contains("kde") || session.contains("plasma") {
|
|
||||||
return DesktopEnvironment::Kde;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopEnvironment::Unknown
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
|
||||||
pub enum LinuxControl {
|
|
||||||
Minimize,
|
|
||||||
Restore,
|
|
||||||
Maximize,
|
|
||||||
Close,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LinuxControl {
|
|
||||||
pub fn as_icon_name(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
LinuxControl::Close => "window-close",
|
|
||||||
LinuxControl::Minimize => "window-minimize",
|
|
||||||
LinuxControl::Maximize => "window-maximize",
|
|
||||||
LinuxControl::Restore => "window-restore",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn linux_controls() -> &'static HashMap<LinuxControl, Option<PathBuf>> {
|
|
||||||
LINUX_CONTROLS.get_or_init(|| {
|
|
||||||
let mut icons = HashMap::new();
|
|
||||||
icons.insert(LinuxControl::Close, None);
|
|
||||||
icons.insert(LinuxControl::Minimize, None);
|
|
||||||
icons.insert(LinuxControl::Maximize, None);
|
|
||||||
icons.insert(LinuxControl::Restore, None);
|
|
||||||
|
|
||||||
let icon_names = [
|
|
||||||
(LinuxControl::Close, vec!["window-close", "dialog-close"]),
|
|
||||||
(
|
|
||||||
LinuxControl::Minimize,
|
|
||||||
vec!["window-minimize", "window-lower"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
LinuxControl::Maximize,
|
|
||||||
vec!["window-maximize", "window-expand"],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
LinuxControl::Restore,
|
|
||||||
vec!["window-restore", "window-return"],
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (control, icon_names) in icon_names {
|
|
||||||
for icon_name in icon_names {
|
|
||||||
// Try GNOME-style naming first
|
|
||||||
let mut control_icon = lookup_icon(format!("{icon_name}-symbolic"))
|
|
||||||
.find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG));
|
|
||||||
|
|
||||||
// If not found, try KDE-style naming
|
|
||||||
if control_icon.is_none() {
|
|
||||||
control_icon = lookup_icon(icon_name)
|
|
||||||
.find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Ok(icon)) = control_icon {
|
|
||||||
icons.entry(control).and_modify(|v| *v = Some(icon.path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
icons
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub mod linux;
|
|
||||||
pub mod windows;
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, px, App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
|
|
||||||
RenderOnce, Rgba, StatefulInteractiveElement, Styled, Window, WindowControlArea,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::h_flex;
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct WindowsWindowControls {
|
|
||||||
button_height: Pixels,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowsWindowControls {
|
|
||||||
pub fn new(button_height: Pixels) -> Self {
|
|
||||||
Self { button_height }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
fn get_font() -> &'static str {
|
|
||||||
"Segoe Fluent Icons"
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
fn get_font() -> &'static str {
|
|
||||||
use windows::Wdk::System::SystemServices::RtlGetVersion;
|
|
||||||
|
|
||||||
let mut version = unsafe { std::mem::zeroed() };
|
|
||||||
let status = unsafe { RtlGetVersion(&mut version) };
|
|
||||||
|
|
||||||
if status.is_ok() && version.dwBuildNumber >= 22000 {
|
|
||||||
"Segoe Fluent Icons"
|
|
||||||
} else {
|
|
||||||
"Segoe MDL2 Assets"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for WindowsWindowControls {
|
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let close_button_hover_color = Rgba {
|
|
||||||
r: 232.0 / 255.0,
|
|
||||||
g: 17.0 / 255.0,
|
|
||||||
b: 32.0 / 255.0,
|
|
||||||
a: 1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
let button_hover_color = cx.theme().ghost_element_hover;
|
|
||||||
let button_active_color = cx.theme().ghost_element_active;
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id("windows-window-controls")
|
|
||||||
.font_family(Self::get_font())
|
|
||||||
.flex()
|
|
||||||
.flex_row()
|
|
||||||
.justify_center()
|
|
||||||
.content_stretch()
|
|
||||||
.max_h(self.button_height)
|
|
||||||
.min_h(self.button_height)
|
|
||||||
.child(WindowsCaptionButton::new(
|
|
||||||
"minimize",
|
|
||||||
WindowsCaptionButtonIcon::Minimize,
|
|
||||||
button_hover_color,
|
|
||||||
button_active_color,
|
|
||||||
))
|
|
||||||
.child(WindowsCaptionButton::new(
|
|
||||||
"maximize-or-restore",
|
|
||||||
if window.is_maximized() {
|
|
||||||
WindowsCaptionButtonIcon::Restore
|
|
||||||
} else {
|
|
||||||
WindowsCaptionButtonIcon::Maximize
|
|
||||||
},
|
|
||||||
button_hover_color,
|
|
||||||
button_active_color,
|
|
||||||
))
|
|
||||||
.child(WindowsCaptionButton::new(
|
|
||||||
"close",
|
|
||||||
WindowsCaptionButtonIcon::Close,
|
|
||||||
close_button_hover_color,
|
|
||||||
button_active_color,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
|
||||||
enum WindowsCaptionButtonIcon {
|
|
||||||
Minimize,
|
|
||||||
Restore,
|
|
||||||
Maximize,
|
|
||||||
Close,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
struct WindowsCaptionButton {
|
|
||||||
id: ElementId,
|
|
||||||
icon: WindowsCaptionButtonIcon,
|
|
||||||
hover_background_color: Hsla,
|
|
||||||
active_background_color: Hsla,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowsCaptionButton {
|
|
||||||
pub fn new(
|
|
||||||
id: impl Into<ElementId>,
|
|
||||||
icon: WindowsCaptionButtonIcon,
|
|
||||||
hover_background_color: impl Into<Hsla>,
|
|
||||||
active_background_color: impl Into<Hsla>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
id: id.into(),
|
|
||||||
icon,
|
|
||||||
hover_background_color: hover_background_color.into(),
|
|
||||||
active_background_color: active_background_color.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for WindowsCaptionButton {
|
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
||||||
h_flex()
|
|
||||||
.id(self.id)
|
|
||||||
.justify_center()
|
|
||||||
.content_center()
|
|
||||||
.occlude()
|
|
||||||
.w(px(36.))
|
|
||||||
.h_full()
|
|
||||||
.text_size(px(10.0))
|
|
||||||
.hover(|style| style.bg(self.hover_background_color))
|
|
||||||
.active(|style| style.bg(self.active_background_color))
|
|
||||||
.map(|this| match self.icon {
|
|
||||||
WindowsCaptionButtonIcon::Close => {
|
|
||||||
this.window_control_area(WindowControlArea::Close)
|
|
||||||
}
|
|
||||||
WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => {
|
|
||||||
this.window_control_area(WindowControlArea::Max)
|
|
||||||
}
|
|
||||||
WindowsCaptionButtonIcon::Minimize => {
|
|
||||||
this.window_control_area(WindowControlArea::Min)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(match self.icon {
|
|
||||||
WindowsCaptionButtonIcon::Minimize => "\u{e921}",
|
|
||||||
WindowsCaptionButtonIcon::Restore => "\u{e923}",
|
|
||||||
WindowsCaptionButtonIcon::Maximize => "\u{e922}",
|
|
||||||
WindowsCaptionButtonIcon::Close => "\u{e8bb}",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@ use gpui::{
|
|||||||
Window,
|
Window,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
use theme::ActiveTheme;
|
||||||
use ui::{h_flex, AxisExt as _, Placement};
|
use ui::{h_flex, AxisExt as _, Placement};
|
||||||
|
|
||||||
use super::{DockArea, PanelEvent};
|
use super::{DockArea, PanelEvent};
|
||||||
@@ -386,7 +386,6 @@ impl Render for StackPanel {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.rounded(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
.bg(cx.theme().elevated_surface_background)
|
||||||
.child(
|
.child(
|
||||||
ResizablePanelGroup::new("stack-panel-group")
|
ResizablePanelGroup::new("stack-panel-group")
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use gpui::{
|
|||||||
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
|
||||||
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TITLEBAR_HEIGHT};
|
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
|
||||||
use ui::button::{Button, ButtonVariants as _};
|
use ui::button::{Button, ButtonVariants as _};
|
||||||
use ui::popup_menu::{PopupMenu, PopupMenuExt};
|
use ui::popup_menu::{PopupMenu, PopupMenuExt};
|
||||||
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
||||||
@@ -594,7 +594,6 @@ impl TabPanel {
|
|||||||
.py_2()
|
.py_2()
|
||||||
.pl_3()
|
.pl_3()
|
||||||
.pr_2()
|
.pr_2()
|
||||||
.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
.when(left_dock_button.is_some(), |this| this.pl_2())
|
.when(left_dock_button.is_some(), |this| this.pl_2())
|
||||||
.when(right_dock_button.is_some(), |this| this.pr_2())
|
.when(right_dock_button.is_some(), |this| this.pr_2())
|
||||||
.when(has_extend_dock_button, |this| {
|
.when(has_extend_dock_button, |this| {
|
||||||
@@ -647,10 +646,6 @@ impl TabPanel {
|
|||||||
TabBar::new()
|
TabBar::new()
|
||||||
.track_scroll(&self.tab_bar_scroll_handle)
|
.track_scroll(&self.tab_bar_scroll_handle)
|
||||||
.h(TITLEBAR_HEIGHT)
|
.h(TITLEBAR_HEIGHT)
|
||||||
.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
.when(self.zoomed, |this| {
|
|
||||||
this.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
|
||||||
})
|
|
||||||
.when(has_extend_dock_button, |this| {
|
.when(has_extend_dock_button, |this| {
|
||||||
this.prefix(
|
this.prefix(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|||||||
Reference in New Issue
Block a user