wip
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m25s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m13s

This commit is contained in:
2026-01-31 09:59:35 +07:00
parent 4336c67d8c
commit 9b03f873c2
28 changed files with 437 additions and 797 deletions

View File

@@ -309,27 +309,21 @@ impl ChatRegistry {
.map(|this| this.downgrade())
}
/// Get all rooms.
pub fn rooms(&self, _cx: &App) -> &Vec<Entity<Room>> {
&self.rooms
}
/// Get all ongoing rooms.
pub fn ongoing_rooms(&self, cx: &App) -> Vec<Entity<Room>> {
/// Get all rooms based on the filter.
pub fn rooms(&self, filter: &RoomKind, cx: &App) -> Vec<Entity<Room>> {
self.rooms
.iter()
.filter(|room| room.read(cx).kind == RoomKind::Ongoing)
.filter(|room| &room.read(cx).kind == filter)
.cloned()
.collect()
}
/// Get all request rooms.
pub fn request_rooms(&self, cx: &App) -> Vec<Entity<Room>> {
/// Count the number of rooms based on the filter.
pub fn count(&self, filter: &RoomKind, cx: &App) -> usize {
self.rooms
.iter()
.filter(|room| room.read(cx).kind != RoomKind::Ongoing)
.cloned()
.collect()
.filter(|room| &room.read(cx).kind == filter)
.count()
}
/// Add a new room to the start of list.

View File

@@ -26,6 +26,3 @@ pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
/// Default timeout (in seconds) for Nostr Connect (Bunker)
pub const BUNKER_TIMEOUT: u64 = 30;
/// Default width of the sidebar.
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;

View File

@@ -35,7 +35,6 @@ theme = { path = "../theme" }
common = { path = "../common" }
state = { path = "../state" }
device = { path = "../device" }
key_store = { path = "../key_store" }
chat = { path = "../chat" }
chat_ui = { path = "../chat_ui" }
settings = { path = "../settings" }

View File

@@ -1,6 +1,4 @@
use gpui::{actions, App};
use key_store::{KeyItem, KeyStore};
use state::NostrRegistry;
use gpui::actions;
// Sidebar actions
actions!(sidebar, [Reload, RelayStatus]);
@@ -19,30 +17,3 @@ actions!(
Quit
]
);
pub fn reset(cx: &mut App) {
let backend = KeyStore::global(cx).read(cx).backend();
let client = NostrRegistry::global(cx).read(cx).client();
cx.spawn(async move |cx| {
// Remove the signer
client.unset_signer().await;
// Delete user's credentials
backend
.delete_credentials(&KeyItem::User.to_string(), cx)
.await
.ok();
// Remove bunker's credentials if available
backend
.delete_credentials(&KeyItem::Bunker.to_string(), cx)
.await
.ok();
cx.update(|cx| {
cx.restart();
});
})
.detach();
}

View File

@@ -82,9 +82,6 @@ fn main() {
// Initialize theme registry
theme::init(cx);
// Initialize backend for keys storage
key_store::init(cx);
// Initialize the nostr client
state::init(cx);

View File

@@ -37,20 +37,13 @@ impl Panel for GreeterPanel {
}
fn title(&self, cx: &App) -> AnyElement {
h_flex()
.gap_1p5()
div()
.child(
svg()
.path("brand/coop.svg")
.size_4()
.text_color(cx.theme().text_muted),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(self.name.clone()),
)
.into_any_element()
}
}

View File

@@ -125,14 +125,7 @@ impl RenderOnce for RoomListItem {
.flex_shrink_0()
.text_xs()
.text_color(cx.theme().text_placeholder)
.when_some(self.created_at, |this, created_at| this.child(created_at))
.when_some(self.kind, |this, kind| {
this.when(kind == RoomKind::Request, |this| {
this.child(
div().size_1().rounded_full().bg(cx.theme().icon_accent),
)
})
}),
.when_some(self.created_at, |this, created_at| this.child(created_at)),
),
)
.hover(|this| this.bg(cx.theme().elevated_surface_background))

View File

@@ -1,36 +1,23 @@
use std::ops::Range;
use std::time::Duration;
use anyhow::{anyhow, Error};
use chat::{ChatEvent, ChatRegistry, Room};
use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use chat::{ChatEvent, ChatRegistry, RoomKind};
use common::RenderedTimestamp;
use dock::panel::{Panel, PanelEvent};
use gpui::prelude::FluentBuilder;
use gpui::{
deferred, div, rems, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
deferred, div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled,
Subscription, Task, Window,
Subscription, Window,
};
use gpui_tokio::Tokio;
use list_item::RoomListItem;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
use ui::avatar::Avatar;
use theme::{ActiveTheme, TABBAR_HEIGHT};
use ui::button::{Button, ButtonVariants};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput};
use ui::{h_flex, v_flex, IconName, Sizable, StyledExt, WindowExtension};
use crate::dialogs::compose::compose_button;
use ui::{h_flex, v_flex, IconName, Selectable, Sizable, StyledExt};
mod list_item;
const FIND_DELAY: u64 = 600;
const FIND_LIMIT: usize = 20;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
cx.new(|cx| Sidebar::new(window, cx))
}
@@ -38,48 +25,25 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
/// Sidebar.
pub struct Sidebar {
name: SharedString,
/// Focus handle for the sidebar
focus_handle: FocusHandle,
/// Image cache
image_cache: Entity<RetainAllImageCache>,
/// Search results
search_results: Entity<Option<Vec<Entity<Room>>>>,
/// Whether there are new chat requests
new_requests: bool,
/// Async search operation
search_task: Option<Task<()>>,
/// Search input state
find_input: Entity<InputState>,
/// Debounced delay for search input
find_debouncer: DebouncedDelay<Self>,
/// Whether searching is in progress
finding: bool,
/// New request flag
new_request: bool,
/// Chatroom filter
filter: Entity<RoomKind>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 2]>,
_subscriptions: SmallVec<[Subscription; 1]>,
}
impl Sidebar {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let search_results = cx.new(|_| None);
// Define the find input state
let find_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Find or start a conversation")
.clean_on_escape()
});
// Get the chat registry
let chat = ChatRegistry::global(cx);
let filter = cx.new(|_| RoomKind::Ongoing);
let mut subscriptions = smallvec![];
@@ -87,52 +51,23 @@ impl Sidebar {
// Subscribe for registry new events
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
if event == &ChatEvent::Ping {
this.new_request = true;
this.new_requests = true;
cx.notify();
};
}),
);
subscriptions.push(
// Subscribe for find input events
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
let delay = Duration::from_millis(FIND_DELAY);
match event {
InputEvent::PressEnter { .. } => {
this.search(window, cx);
}
InputEvent::Change => {
if state.read(cx).value().is_empty() {
// Clear the result when input is empty
this.clear(window, cx);
} else {
// Run debounced search
this.find_debouncer
.fire_new(delay, window, cx, |this, window, cx| {
this.debounced_search(window, cx)
});
}
}
_ => {}
};
}),
);
Self {
name: "Sidebar".into(),
focus_handle: cx.focus_handle(),
image_cache: RetainAllImageCache::new(cx),
find_debouncer: DebouncedDelay::new(),
finding: false,
new_request: false,
find_input,
search_results,
search_task: None,
new_requests: false,
filter,
_subscriptions: subscriptions,
}
}
/*
async fn nip50(client: &Client, query: &str) -> Result<Vec<Event>, Error> {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
@@ -405,31 +340,30 @@ impl Sidebar {
cx.notify();
});
}
*/
fn open(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
/// Get the active filter.
fn current_filter(&self, kind: &RoomKind, cx: &Context<Self>) -> bool {
self.filter.read(cx) == kind
}
/// Set the active filter for the sidebar.
fn set_filter(&mut self, kind: RoomKind, cx: &mut Context<Self>) {
self.filter.update(cx, |this, cx| {
*this = kind;
cx.notify();
});
self.new_requests = false;
}
/// Open a room.
fn open(&mut self, id: u64, _window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx);
match chat.read(cx).room(&id, cx) {
Some(room) => {
chat.update(cx, |this, cx| {
this.emit_room(room, cx);
});
}
None => {
if let Some(room) = self
.search_results
.read(cx)
.as_ref()
.and_then(|results| results.iter().find(|this| this.read(cx).id == id))
.map(|this| this.downgrade())
{
chat.update(cx, |this, cx| {
this.emit_room(room, cx);
});
// Clear all search results
self.clear(window, cx);
}
}
if let Some(room) = chat.read(cx).room(&id, cx) {
chat.update(cx, |this, cx| {
this.emit_room(room, cx);
});
}
}
@@ -439,15 +373,8 @@ impl Sidebar {
_window: &Window,
cx: &Context<Self>,
) -> Vec<impl IntoElement> {
// Get rooms from search results first
let rooms = match self.search_results.read(cx).as_ref() {
Some(results) => results,
None => {
// Fallback to chat registry if there are no search results
let chat = ChatRegistry::global(cx);
chat.read(cx).rooms(cx)
}
};
let chat = ChatRegistry::global(cx);
let rooms = chat.read(cx).rooms(self.filter.read(cx), cx);
rooms
.get(range.clone())
@@ -456,8 +383,8 @@ impl Sidebar {
.enumerate()
.map(|(ix, item)| {
let room = item.read(cx);
let id = room.id;
let public_key = room.display_member(cx).public_key();
let id = room.id;
let handler = cx.listener({
move |this, _ev, window, cx| {
@@ -494,73 +421,89 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading();
// Get total rooms
let total_rooms = match self.search_results.read(cx).as_ref() {
Some(results) => results.len(),
None => chat.read(cx).rooms(cx).len(),
};
let total_rooms = chat.read(cx).count(self.filter.read(cx), cx);
v_flex()
.image_cache(self.image_cache.clone())
.size_full()
.relative()
.gap_2()
.bg(cx.theme().surface_background)
// Titlebar
.child(
h_flex()
.px_2p5()
.h(TABBAR_HEIGHT)
.w_full()
.border_b_1()
.border_color(cx.theme().border)
.child(
h_flex()
.w_full()
.flex_1()
.h_full()
.gap_2()
.justify_between()
.when_some(identity.read(cx).public_key, |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
this.child(
h_flex()
.gap_1p5()
.text_xs()
.text_color(cx.theme().text_muted)
.child(Avatar::new(profile.avatar()).size(rems(1.25)))
.child(profile.name()),
)
})
.child(compose_button()),
.p_2()
.justify_center()
.child(
Button::new("all")
.map(|this| {
if self.current_filter(&RoomKind::Ongoing, cx) {
this.icon(IconName::InboxFill)
} else {
this.icon(IconName::Inbox)
}
})
.label("Inbox")
.tooltip("All ongoing conversations")
.xsmall()
.bold()
.ghost()
.flex_1()
.rounded_none()
.selected(self.current_filter(&RoomKind::Ongoing, cx))
.on_click(cx.listener(|this, _, _, cx| {
this.set_filter(RoomKind::Ongoing, cx);
})),
)
.child(
Button::new("requests")
.map(|this| {
if self.current_filter(&RoomKind::Request, cx) {
this.icon(IconName::FistbumpFill)
} else {
this.icon(IconName::Fistbump)
}
})
.label("Requests")
.tooltip("Incoming new conversations")
.xsmall()
.bold()
.ghost()
.flex_1()
.rounded_none()
.selected(!self.current_filter(&RoomKind::Ongoing, cx))
.when(self.new_requests, |this| {
this.child(
div().size_1().rounded_full().bg(cx.theme().cursor),
)
})
.on_click(cx.listener(|this, _, _, cx| {
this.set_filter(RoomKind::default(), cx);
})),
),
)
.h(TITLEBAR_HEIGHT),
)
// Search Input
.child(
div().px_2p5().child(
TextInput::new(&self.find_input)
.small()
.cleanable()
.appearance(true)
.text_xs()
.flex_none()
.map(|this| {
if !self.find_input.read(cx).loading {
this.suffix(
Button::new("find")
.icon(IconName::Search)
.tooltip("Press Enter to search")
.transparent()
.small(),
)
} else {
this
}
}),
),
.child(
h_flex()
.h_full()
.px_2()
.border_l_1()
.border_color(cx.theme().border)
.child(
Button::new("option")
.icon(IconName::Ellipsis)
.small()
.ghost(),
),
),
)
.when(!loading && total_rooms == 0, |this| {
this.child(
@@ -590,7 +533,6 @@ impl Render for Sidebar {
)),
)
})
// Chat Rooms
.child(
v_flex()
.px_1p5()

View File

@@ -1,33 +1,36 @@
use std::sync::Arc;
use std::time::Duration;
use chat::{ChatEvent, ChatRegistry};
use chat_ui::{CopyPublicKey, OpenPublicKey};
use common::DEFAULT_SIDEBAR_WIDTH;
use chat::{ChatEvent, ChatRegistry, Room};
use common::DebouncedDelay;
use dock::dock::DockPlacement;
use dock::panel::PanelView;
use dock::{ClosePanel, DockArea, DockItem};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, relative, App, AppContext, Axis, ClipboardItem, Context, Entity, InteractiveElement,
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Task, Window,
};
use nostr_connect::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec};
use theme::{ActiveTheme, Theme, ThemeRegistry};
use state::NostrRegistry;
use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
use titlebar::TitleBar;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::modal::ModalButtonProps;
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension};
use crate::actions::{reset, KeyringPopup, Logout, Themes};
use crate::dialogs::profile;
use crate::panels::greeter;
use crate::sidebar;
const FIND_DELAY: u64 = 600;
const FIND_LIMIT: usize = 20;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
cx.new(|cx| Workspace::new(window, cx))
}
#[derive(Debug)]
pub struct Workspace {
/// App's Title Bar
titlebar: Entity<TitleBar>,
@@ -35,17 +38,46 @@ pub struct Workspace {
/// App's Dock Area
dock: Entity<DockArea>,
/// Search results
search_results: Entity<Option<Vec<Entity<Room>>>>,
/// Async search operation
search_task: Option<Task<()>>,
/// Search input state
find_input: Entity<InputState>,
/// Debounced delay for search input
find_debouncer: DebouncedDelay<Self>,
/// Whether searching is in progress
finding: bool,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 3]>,
_subscriptions: SmallVec<[Subscription; 4]>,
}
impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx);
// App's titlebar
let titlebar = cx.new(|_| TitleBar::new());
// App's dock area
let dock =
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
// Define the find input state
let find_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Find or start a conversation")
.clean_on_escape()
});
// Define the search results states
let search_results = cx.new(|_| None);
let mut subscriptions = smallvec![];
subscriptions.push(
@@ -91,7 +123,7 @@ impl Workspace {
subscriptions.push(
// Observe the chat registry
cx.observe(&chat, move |this, chat, cx| {
let ids = this.get_all_panels(cx);
let ids = this.panel_ids(cx);
chat.update(cx, |this, cx| {
this.refresh_rooms(ids, cx);
@@ -99,6 +131,32 @@ impl Workspace {
}),
);
subscriptions.push(
// Subscribe for find input events
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
let delay = Duration::from_millis(FIND_DELAY);
match event {
InputEvent::PressEnter { .. } => {
// this.search(window, cx);
}
InputEvent::Change => {
if state.read(cx).value().is_empty() {
// Clear the result when input is empty
// this.clear(window, cx);
} else {
// Run debounced search
//this.find_debouncer
// .fire_new(delay, window, cx, |this, window, cx| {
// this.debounced_search(window, cx)
// });
}
}
_ => {}
};
}),
);
// Set the default layout for app's dock
cx.defer_in(window, |this, window, cx| {
this.set_layout(window, cx);
@@ -107,6 +165,11 @@ impl Workspace {
Self {
titlebar,
dock,
find_debouncer: DebouncedDelay::new(),
finding: false,
find_input,
search_results,
search_task: None,
_subscriptions: subscriptions,
}
}
@@ -126,6 +189,19 @@ impl Workspace {
}
}
fn panel_ids(&self, cx: &App) -> Option<Vec<u64>> {
let ids: Vec<u64> = self
.dock
.read(cx)
.items
.panel_ids(cx)
.into_iter()
.filter_map(|panel| panel.parse::<u64>().ok())
.collect();
Some(ids)
}
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let weak_dock = self.dock.downgrade();
@@ -150,123 +226,59 @@ impl Workspace {
// Update the dock layout
self.dock.update(cx, |this, cx| {
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
this.set_left_dock(left, Some(SIDEBAR_WIDTH), true, window, cx);
this.set_center(center, window, cx);
});
}
fn on_themes(&mut self, _ev: &Themes, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, move |this, _window, cx| {
let registry = ThemeRegistry::global(cx);
let themes = registry.read(cx).themes();
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
this.title("Select theme")
.show_close(true)
.overlay_closable(true)
.child(v_flex().gap_2().pb_4().children({
let mut items = Vec::with_capacity(themes.len());
h_flex()
.h(TITLEBAR_HEIGHT)
.flex_1()
.justify_between()
.gap_2()
.when_some(identity.read(cx).public_key, |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx);
for (name, theme) in themes.iter() {
items.push(
h_flex()
.h_10()
.justify_between()
.child(
v_flex()
.child(
div()
.text_sm()
.text_color(cx.theme().text)
.line_height(relative(1.3))
.child(theme.name.clone()),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(theme.author.clone()),
),
)
.child(
Button::new(format!("change-{name}"))
.label("Set")
.small()
.ghost()
.on_click({
let theme = theme.clone();
move |_ev, window, cx| {
Theme::apply_theme(theme.clone(), Some(window), cx);
}
}),
),
);
}
items
}))
})
}
fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) {
reset(cx);
}
fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context<Self>) {
let public_key = ev.0;
let view = profile::init(public_key, window, cx);
window.open_modal(cx, move |this, _window, _cx| {
this.alert()
.show_close(true)
.overlay_closable(true)
.child(view.clone())
.button_props(ModalButtonProps::default().ok_text("View on njump.me"))
.on_ok(move |_, _window, cx| {
let bech32 = public_key.to_bech32().unwrap();
let url = format!("https://njump.me/{bech32}");
// Open the URL in the default browser
cx.open_url(&url);
// false to keep the modal open
false
})
});
}
fn on_copy_pubkey(&mut self, ev: &CopyPublicKey, window: &mut Window, cx: &mut Context<Self>) {
let Ok(bech32) = ev.0.to_bech32();
cx.write_to_clipboard(ClipboardItem::new_string(bech32));
window.push_notification("Copied", cx);
}
fn on_keyring(&mut self, _ev: &KeyringPopup, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, move |this, _window, _cx| {
this.show_close(true)
.title(SharedString::from("Keyring is disabled"))
.child(
v_flex()
.gap_2()
.pb_4()
.text_sm()
.child(SharedString::from("Coop cannot access the Keyring Service on your system. By design, Coop uses Keyring to store your credentials."))
.child(SharedString::from("Without access to Keyring, Coop will store your credentials as plain text."))
.child(SharedString::from("If you want to store your credentials in the Keyring, please enable Keyring and allow Coop to access it.")),
this.child(
h_flex()
.gap_0p5()
.child(Avatar::new(profile.avatar()).size(rems(1.25)))
.child(
Icon::new(IconName::ChevronDown)
.small()
.text_color(cx.theme().text_muted),
),
)
});
})
}
fn get_all_panels(&self, cx: &App) -> Option<Vec<u64>> {
let ids: Vec<u64> = self
.dock
.read(cx)
.items
.panel_ids(cx)
.into_iter()
.filter_map(|panel| panel.parse::<u64>().ok())
.collect();
fn titlebar_center(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
h_flex().flex_1().child(
TextInput::new(&self.find_input)
.xsmall()
.cleanable()
.appearance(true)
.bordered(false)
.text_xs()
.when(!self.find_input.read(cx).loading, |this| {
this.prefix(
Button::new("find")
.icon(IconName::Search)
.tooltip("Press Enter to search")
.transparent()
.small(),
)
}),
)
}
Some(ids)
fn titlebar_right(&mut self, _window: &mut Window, _cx: &Context<Self>) -> impl IntoElement {
h_flex().flex_1()
}
}
@@ -275,13 +287,18 @@ impl Render for Workspace {
let modal_layer = Root::render_modal_layer(window, cx);
let notification_layer = Root::render_notification_layer(window, cx);
// Titlebar elements
let left = self.titlebar_left(window, cx).into_any_element();
let center = self.titlebar_center(window, cx).into_any_element();
let right = self.titlebar_right(window, cx).into_any_element();
// Update title bar children
self.titlebar.update(cx, |this, _cx| {
this.set_children(vec![left, center, right]);
});
div()
.id(SharedString::from("workspace"))
.on_action(cx.listener(Self::on_themes))
.on_action(cx.listener(Self::on_sign_out))
.on_action(cx.listener(Self::on_open_pubkey))
.on_action(cx.listener(Self::on_copy_pubkey))
.on_action(cx.listener(Self::on_keyring))
.relative()
.size_full()
.child(

View File

@@ -3,7 +3,7 @@ use gpui::{
div, px, AnyElement, App, Div, InteractiveElement, IntoElement, MouseButton, ParentElement,
RenderOnce, StatefulInteractiveElement, Styled, Window,
};
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
use theme::{ActiveTheme, TABBAR_HEIGHT};
use ui::{Selectable, Sizable, Size};
pub mod tab_bar;
@@ -105,37 +105,37 @@ impl Sizable for Tab {
impl RenderOnce for Tab {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let (text_color, bg_color, hover_bg_color, border_color) =
let (text_color, hover_text_color, bg_color, border_color) =
match (self.selected, self.disabled) {
(true, false) => (
cx.theme().text,
cx.theme().tab_active_foreground,
cx.theme().tab_hover_foreground,
cx.theme().tab_active_background,
cx.theme().tab_hover_background,
cx.theme().border,
),
(false, false) => (
cx.theme().text_muted,
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
cx.theme().border_transparent,
),
(true, true) => (
cx.theme().text_muted,
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
cx.theme().border_disabled,
),
(false, true) => (
cx.theme().text_muted,
cx.theme().tab_inactive_foreground,
cx.theme().tab_hover_foreground,
cx.theme().ghost_element_background,
cx.theme().tab_hover_background,
cx.theme().border_disabled,
),
};
self.base
.id(self.ix)
.h(TITLEBAR_HEIGHT)
.h(TABBAR_HEIGHT)
.px_4()
.relative()
.flex()
@@ -151,7 +151,7 @@ impl RenderOnce for Tab {
.border_r(px(1.))
.border_color(border_color)
.when(!self.selected && !self.disabled, |this| {
this.hover(|this| this.text_color(text_color).bg(hover_bg_color))
this.hover(|this| this.text_color(hover_text_color))
})
.when_some(self.prefix, |this, prefix| {
this.child(prefix).text_color(text_color)

View File

@@ -91,18 +91,12 @@ impl Sizable for TabBar {
}
impl RenderOnce for TabBar {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let focused = window.is_window_active();
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
self.base
.group("tab-bar")
.relative()
.refine_style(&self.style)
.bg(cx.theme().elevated_surface_background)
.when(!focused, |this| {
// TODO: add specific styles for unfocused tab bar
this.bg(cx.theme().elevated_surface_background)
})
.bg(cx.theme().surface_background)
.child(
div()
.id("border-bottom")

View File

@@ -7,7 +7,7 @@ use gpui::{
MouseButton, ParentElement, Pixels, Render, ScrollHandle, SharedString,
StatefulInteractiveElement, Styled, WeakEntity, Window,
};
use theme::{ActiveTheme, TITLEBAR_HEIGHT};
use theme::{ActiveTheme, TABBAR_HEIGHT};
use ui::button::{Button, ButtonVariants as _};
use ui::popup_menu::{PopupMenu, PopupMenuExt};
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
@@ -645,7 +645,7 @@ impl TabPanel {
TabBar::new()
.track_scroll(&self.tab_bar_scroll_handle)
.h(TITLEBAR_HEIGHT)
.h(TABBAR_HEIGHT)
.when(has_extend_dock_button, |this| {
this.prefix(
h_flex()
@@ -656,7 +656,7 @@ impl TabPanel {
.border_b_1()
.h_full()
.border_color(cx.theme().border)
.bg(cx.theme().elevated_surface_background)
.bg(cx.theme().surface_background)
.px_2()
.children(left_dock_button)
.children(bottom_dock_button),

View File

@@ -1,19 +0,0 @@
[package]
name = "key_store"
version.workspace = true
edition.workspace = true
publish.workspace = true
[dependencies]
common = { path = "../common" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
smallvec.workspace = true
smol.workspace = true
log.workspace = true
futures.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -1,211 +0,0 @@
use std::any::Any;
use std::collections::HashMap;
use std::fmt::Display;
use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use anyhow::Result;
use common::config_dir;
use futures::FutureExt as _;
use gpui::AsyncApp;
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Credential {
public_key: PublicKey,
secret: String,
}
impl Credential {
pub fn new(user: String, secret: Vec<u8>) -> Self {
Self {
public_key: PublicKey::parse(&user).unwrap(),
secret: String::from_utf8(secret).unwrap(),
}
}
pub fn public_key(&self) -> PublicKey {
self.public_key
}
pub fn secret(&self) -> &str {
&self.secret
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyItem {
User,
Bunker,
}
impl Display for KeyItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::User => write!(f, "coop-user"),
Self::Bunker => write!(f, "coop-bunker"),
}
}
}
impl From<KeyItem> for String {
fn from(item: KeyItem) -> Self {
item.to_string()
}
}
pub trait KeyBackend: Any + Send + Sync {
fn name(&self) -> &str;
/// Reads the credentials from the provider.
#[allow(clippy::type_complexity)]
fn read_credentials<'a>(
&'a self,
url: &'a str,
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>>;
/// Writes the credentials to the provider.
fn write_credentials<'a>(
&'a self,
url: &'a str,
username: &'a str,
password: &'a [u8],
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
/// Deletes the credentials from the provider.
fn delete_credentials<'a>(
&'a self,
url: &'a str,
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>>;
}
/// A credentials provider that stores credentials in the system keychain.
pub struct KeyringProvider;
impl KeyBackend for KeyringProvider {
fn name(&self) -> &str {
"keyring"
}
fn read_credentials<'a>(
&'a self,
url: &'a str,
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
async move { cx.update(|cx| cx.read_credentials(url)).await }.boxed_local()
}
fn write_credentials<'a>(
&'a self,
url: &'a str,
username: &'a str,
password: &'a [u8],
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
cx.update(move |cx| cx.write_credentials(url, username, password))
.await
}
.boxed_local()
}
fn delete_credentials<'a>(
&'a self,
url: &'a str,
cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move { cx.update(move |cx| cx.delete_credentials(url)).await }.boxed_local()
}
}
/// A credentials provider that stores credentials in a local file.
pub struct FileProvider {
path: PathBuf,
}
impl FileProvider {
pub fn new() -> Self {
let path = config_dir().join(".keys");
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
Self { path }
}
pub fn load_credentials(&self) -> Result<HashMap<String, (String, Vec<u8>)>> {
let json = std::fs::read(&self.path)?;
let credentials: HashMap<String, (String, Vec<u8>)> = serde_json::from_slice(&json)?;
Ok(credentials)
}
pub fn save_credentials(&self, credentials: &HashMap<String, (String, Vec<u8>)>) -> Result<()> {
let json = serde_json::to_string(credentials)?;
std::fs::write(&self.path, json)?;
Ok(())
}
}
impl Default for FileProvider {
fn default() -> Self {
Self::new()
}
}
impl KeyBackend for FileProvider {
fn name(&self) -> &str {
"file"
}
fn read_credentials<'a>(
&'a self,
url: &'a str,
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<Option<(String, Vec<u8>)>>> + 'a>> {
async move {
Ok(self
.load_credentials()
.unwrap_or_default()
.get(url)
.cloned())
}
.boxed_local()
}
fn write_credentials<'a>(
&'a self,
url: &'a str,
username: &'a str,
password: &'a [u8],
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
let mut credentials = self.load_credentials().unwrap_or_default();
credentials.insert(url.to_string(), (username.to_string(), password.to_vec()));
self.save_credentials(&credentials)
}
.boxed_local()
}
fn delete_credentials<'a>(
&'a self,
url: &'a str,
_cx: &'a AsyncApp,
) -> Pin<Box<dyn Future<Output = Result<()>> + 'a>> {
async move {
let mut credentials = self.load_credentials()?;
credentials.remove(url);
self.save_credentials(&credentials)
}
.boxed_local()
}
}

View File

@@ -1,94 +0,0 @@
use std::sync::{Arc, LazyLock};
pub use backend::*;
use gpui::{App, AppContext, Context, Entity, Global, Task};
use smallvec::{smallvec, SmallVec};
mod backend;
static DISABLE_KEYRING: LazyLock<bool> =
LazyLock::new(|| std::env::var("DISABLE_KEYRING").is_ok_and(|value| !value.is_empty()));
pub fn init(cx: &mut App) {
KeyStore::set_global(cx.new(KeyStore::new), cx);
}
struct GlobalKeyStore(Entity<KeyStore>);
impl Global for GlobalKeyStore {}
pub struct KeyStore {
/// Key Store for storing credentials
pub backend: Arc<dyn KeyBackend>,
/// Whether the keystore has been initialized
pub initialized: bool,
/// Tasks for asynchronous operations
_tasks: SmallVec<[Task<()>; 1]>,
}
impl KeyStore {
/// Retrieve the global keys state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalKeyStore>().0.clone()
}
/// Set the global keys instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalKeyStore(state));
}
/// Create a new keys instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
// Use the file system for keystore in development or when the user specifies it
let use_file_keystore = cfg!(debug_assertions) || *DISABLE_KEYRING;
// Construct the key backend
let backend: Arc<dyn KeyBackend> = if use_file_keystore {
Arc::new(FileProvider::default())
} else {
Arc::new(KeyringProvider)
};
// Only used for testing keyring availability on the user's system
let read_credential = cx.read_credentials("Coop");
let mut tasks = smallvec![];
tasks.push(
// Verify the keyring availability
cx.spawn(async move |this, cx| {
let result = read_credential.await;
this.update(cx, |this, cx| {
if let Err(e) = result {
log::error!("Keyring error: {e}");
// For Linux:
// The user has not installed secret service on their system
// Fall back to the file provider
this.backend = Arc::new(FileProvider::default());
}
this.initialized = true;
cx.notify();
})
.ok();
}),
);
Self {
backend,
initialized: false,
_tasks: tasks,
}
}
/// Returns the key backend.
pub fn backend(&self) -> Arc<dyn KeyBackend> {
Arc::clone(&self.backend)
}
/// Returns true if the keystore is a file key backend.
pub fn is_using_file_keystore(&self) -> bool {
self.backend.name() == "file"
}
}

View File

@@ -75,8 +75,10 @@ pub struct ThemeColors {
// Tab colors
pub tab_inactive_background: Hsla,
pub tab_hover_background: Hsla,
pub tab_inactive_foreground: Hsla,
pub tab_active_background: Hsla,
pub tab_active_foreground: Hsla,
pub tab_hover_foreground: Hsla,
// Scrollbar colors
pub scrollbar_thumb_background: Hsla,
@@ -107,7 +109,7 @@ impl ThemeColors {
background: neutral().dark().step_1(),
surface_background: neutral().dark().step_2(),
elevated_surface_background: neutral().dark().step_3(),
panel_background: neutral().dark().step_1(),
panel_background: neutral().dark().step_3(),
overlay: neutral().dark_alpha().step_3(),
border: neutral().dark().step_6(),
@@ -163,8 +165,10 @@ impl ThemeColors {
ghost_element_disabled: neutral().dark_alpha().step_2(),
tab_inactive_background: neutral().dark().step_2(),
tab_hover_background: neutral().dark().step_3(),
tab_active_background: neutral().dark().step_1(),
tab_inactive_foreground: neutral().dark().step_11(),
tab_active_background: neutral().dark().step_3(),
tab_active_foreground: neutral().dark().step_12(),
tab_hover_foreground: brand().dark().step_9(),
scrollbar_thumb_background: neutral().dark_alpha().step_3(),
scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(),
@@ -176,8 +180,8 @@ impl ThemeColors {
cursor: hsl(200., 100., 50.),
selection: hsl(200., 100., 50.).alpha(0.25),
titlebar: neutral().dark().step_2(),
titlebar_inactive: neutral().dark().step_3(),
titlebar: neutral().dark_alpha().step_1(),
titlebar_inactive: neutral().dark_alpha().step_2(),
}
}
}

View File

@@ -29,6 +29,15 @@ pub const CLIENT_SIDE_DECORATION_BORDER: Pixels = px(1.0);
/// Defines window titlebar height
pub const TITLEBAR_HEIGHT: Pixels = px(36.0);
/// Defines tabbar height
pub const TABBAR_HEIGHT: Pixels = px(30.);
/// Defines default sidebar width
pub const SIDEBAR_WIDTH: Pixels = px(240.);
/// Defines search input width
pub const SEARCH_INPUT_WIDTH: Pixels = px(420.);
pub fn init(cx: &mut App) {
registry::init(cx);

View File

@@ -1,12 +1,11 @@
use std::mem;
use gpui::prelude::FluentBuilder;
#[cfg(target_os = "linux")]
use gpui::MouseButton;
#[cfg(not(target_os = "windows"))]
use gpui::Pixels;
use gpui::{
div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
ParentElement, Pixels, Render, StatefulInteractiveElement as _, Styled, Window,
WindowControlArea,
px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
ParentElement, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea,
};
use smallvec::{smallvec, SmallVec};
use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING};
@@ -14,19 +13,18 @@ use ui::h_flex;
#[cfg(target_os = "linux")]
use crate::platforms::linux::LinuxWindowControls;
use crate::platforms::mac::TRAFFIC_LIGHT_PADDING;
use crate::platforms::windows::WindowsWindowControls;
mod platforms;
/// Titlebar
pub struct TitleBar {
/// Children elements of the title bar.
children: SmallVec<[AnyElement; 2]>,
should_move: bool,
}
impl Default for TitleBar {
fn default() -> Self {
Self::new()
}
/// Whether the title bar is currently being moved.
should_move: bool,
}
impl TitleBar {
@@ -38,16 +36,16 @@ impl TitleBar {
}
#[cfg(not(target_os = "windows"))]
pub fn height(window: &mut Window) -> Pixels {
pub fn height(&self, window: &mut Window) -> Pixels {
(1.75 * window.rem_size()).max(px(34.))
}
#[cfg(target_os = "windows")]
pub fn height(_window: &mut Window) -> Pixels {
pub fn height(&self, _window: &mut Window) -> Pixels {
px(32.)
}
pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
pub fn titlebar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
if window.is_window_active() && !self.should_move {
cx.theme().titlebar
@@ -67,20 +65,21 @@ impl TitleBar {
}
}
impl ParentElement for TitleBar {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
impl Default for TitleBar {
fn default() -> Self {
Self::new()
}
}
impl Render for TitleBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let height = self.height(window);
let color = self.titlebar_color(window, cx);
let children = std::mem::take(&mut self.children);
#[cfg(target_os = "linux")]
let supported_controls = window.window_controls();
let decorations = window.window_decorations();
let height = Self::height(window);
let color = self.title_bar_color(window, cx);
let children = mem::take(&mut self.children);
h_flex()
.window_control_area(WindowControlArea::Drag)
@@ -90,11 +89,7 @@ impl Render for TitleBar {
if window.is_fullscreen() {
this.px_2()
} else if cx.theme().platform.is_mac() {
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
.pr_2()
.when(children.len() <= 1, |this| {
this.pr(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
})
this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING))
} else {
this.px_2()
}
@@ -114,11 +109,8 @@ impl Render for TitleBar {
.border_color(cx.theme().border)
.content_stretch()
.child(
div()
h_flex()
.id("title-bar")
.flex()
.flex_row()
.items_center()
.justify_between()
.w_full()
.when(cx.theme().platform.is_mac(), |this| {

View File

@@ -25,20 +25,26 @@ impl LinuxWindowControls {
impl RenderOnce for LinuxWindowControls {
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
let supported_controls = window.window_controls();
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)
}
.when(supported_controls.minimize, |this| {
this.child(WindowControl::new(
LinuxControl::Minimize,
IconName::WindowMinimize,
))
})
.when(supported_controls.maximize, |this| {
this.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)

View File

@@ -12,6 +12,7 @@ pub enum IconName {
ArrowLeft,
ArrowRight,
Boom,
ChevronDown,
CaretDown,
CaretRight,
CaretUp,
@@ -27,6 +28,8 @@ pub enum IconName {
Eye,
Info,
Invite,
Inbox,
InboxFill,
Link,
Loader,
Moon,
@@ -52,6 +55,8 @@ pub enum IconName {
WindowMaximize,
WindowMinimize,
WindowRestore,
Fistbump,
FistbumpFill,
Zoom,
}
@@ -61,6 +66,7 @@ impl IconName {
Self::ArrowLeft => "icons/arrow-left.svg",
Self::ArrowRight => "icons/arrow-right.svg",
Self::Boom => "icons/boom.svg",
Self::ChevronDown => "icons/chevron-down.svg",
Self::CaretDown => "icons/caret-down.svg",
Self::CaretRight => "icons/caret-right.svg",
Self::CaretUp => "icons/caret-up.svg",
@@ -76,6 +82,8 @@ impl IconName {
Self::Eye => "icons/eye.svg",
Self::Info => "icons/info.svg",
Self::Invite => "icons/invite.svg",
Self::Inbox => "icons/inbox.svg",
Self::InboxFill => "icons/inbox-fill.svg",
Self::Link => "icons/link.svg",
Self::Loader => "icons/loader.svg",
Self::Moon => "icons/moon.svg",
@@ -101,6 +109,8 @@ impl IconName {
Self::WindowMaximize => "icons/window-maximize.svg",
Self::WindowMinimize => "icons/window-minimize.svg",
Self::WindowRestore => "icons/window-restore.svg",
Self::Fistbump => "icons/fistbump.svg",
Self::FistbumpFill => "icons/fistbump-fill.svg",
Self::Zoom => "icons/zoom.svg",
}
.into()

View File

@@ -145,6 +145,7 @@ impl Styled for TextInput {
impl RenderOnce for TextInput {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
const LINE_HEIGHT: Rems = Rems(1.25);
let font = window.text_style().font();
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
@@ -155,6 +156,7 @@ impl RenderOnce for TextInput {
});
let state = self.state.read(cx);
let focused = state.focus_handle.is_focused(window) && !state.disabled;
let gap_x = match self.size {
Size::Small => px(4.),
@@ -266,7 +268,16 @@ impl RenderOnce for TextInput {
.when_some(self.height, |this, height| this.h(height))
})
.when(self.appearance, |this| {
this.bg(bg).rounded(cx.theme().radius)
this.bg(bg)
.rounded(cx.theme().radius)
.when(self.bordered, |this| {
this.border_color(cx.theme().border)
.border_1()
.when(cx.theme().shadow, |this| this.shadow_xs())
.when(focused && self.focus_bordered, |this| {
this.border_color(cx.theme().border_focused)
})
})
})
.items_center()
.gap(gap_x)

View File

@@ -2,10 +2,10 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
canvas, div, point, px, rgba, AnyView, App, AppContext, Bounds, Context, CursorStyle,
canvas, div, point, px, size, AnyView, App, AppContext, Bounds, Context, CursorStyle,
Decorations, Edges, Entity, FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement,
MouseButton, ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled,
WeakFocusHandle, Window,
Tiling, WeakFocusHandle, Window,
};
use theme::{
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
@@ -241,7 +241,10 @@ impl Render for Root {
window.set_rem_size(rem_size);
// Set the client inset (linux only)
window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW);
match decorations {
Decorations::Client { .. } => window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW),
Decorations::Server => window.set_client_inset(px(0.0)),
}
div()
.id("window")
@@ -267,7 +270,7 @@ impl Render for Root {
let size = window.window_bounds().get_bounds().size;
let Some(edge) =
resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size)
resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
else {
return;
};
@@ -314,7 +317,9 @@ impl Render for Root {
let size = window.window_bounds().get_bounds().size;
let pos = e.position;
if let Some(edge) = resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) {
if let Some(edge) =
resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
{
window.start_window_resize(edge)
};
}),
@@ -324,9 +329,7 @@ impl Render for Root {
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.when(!cx.theme().platform.is_mac(), |div| {
div.border_color(rgba(0x64748b33))
})
.border_color(cx.theme().border)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
})
@@ -401,25 +404,59 @@ pub(crate) fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
}
/// Get the window resize edge.
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
let edge = if pos.y < shadow_size && pos.x < shadow_size {
ResizeEdge::TopLeft
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::TopRight
} else if pos.y < shadow_size {
ResizeEdge::Top
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
ResizeEdge::BottomLeft
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::BottomRight
} else if pos.y > size.height - shadow_size {
ResizeEdge::Bottom
} else if pos.x < shadow_size {
ResizeEdge::Left
} else if pos.x > size.width - shadow_size {
ResizeEdge::Right
} else {
fn resize_edge(
pos: Point<Pixels>,
shadow_size: Pixels,
window_size: Size<Pixels>,
tiling: Tiling,
) -> Option<ResizeEdge> {
let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
if bounds.contains(&pos) {
return None;
};
Some(edge)
}
let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
if !tiling.top && top_left_bounds.contains(&pos) {
return Some(ResizeEdge::TopLeft);
}
let top_right_bounds = Bounds::new(
Point::new(window_size.width - corner_size.width, px(0.)),
corner_size,
);
if !tiling.top && top_right_bounds.contains(&pos) {
return Some(ResizeEdge::TopRight);
}
let bottom_left_bounds = Bounds::new(
Point::new(px(0.), window_size.height - corner_size.height),
corner_size,
);
if !tiling.bottom && bottom_left_bounds.contains(&pos) {
return Some(ResizeEdge::BottomLeft);
}
let bottom_right_bounds = Bounds::new(
Point::new(
window_size.width - corner_size.width,
window_size.height - corner_size.height,
),
corner_size,
);
if !tiling.bottom && bottom_right_bounds.contains(&pos) {
return Some(ResizeEdge::BottomRight);
}
if !tiling.top && pos.y < shadow_size {
Some(ResizeEdge::Top)
} else if !tiling.bottom && pos.y > window_size.height - shadow_size {
Some(ResizeEdge::Bottom)
} else if !tiling.left && pos.x < shadow_size {
Some(ResizeEdge::Left)
} else if !tiling.right && pos.x > window_size.width - shadow_size {
Some(ResizeEdge::Right)
} else {
None
}
}