wip
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m25s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m13s
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:
@@ -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.
|
||||
|
||||
@@ -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.;
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user