update deps
Some checks failed
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (ubuntu-latest, stable) (push) Has been cancelled

This commit is contained in:
2026-02-21 07:53:24 +07:00
parent b88955e62c
commit 4c0beb2a2a
18 changed files with 1434 additions and 304 deletions

1304
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,9 @@ publish = false
# GPUI # GPUI
gpui = { git = "https://github.com/zed-industries/zed" } gpui = { git = "https://github.com/zed-industries/zed" }
gpui_platform = { git = "https://github.com/zed-industries/zed" } gpui_platform = { git = "https://github.com/zed-industries/zed" }
gpui_linux = { git = "https://github.com/zed-industries/zed" }
gpui_windows = { git = "https://github.com/zed-industries/zed" }
gpui_macos = { git = "https://github.com/zed-industries/zed" }
gpui_tokio = { git = "https://github.com/zed-industries/zed" } gpui_tokio = { git = "https://github.com/zed-industries/zed" }
reqwest_client = { git = "https://github.com/zed-industries/zed" } reqwest_client = { git = "https://github.com/zed-industries/zed" }
@@ -29,7 +32,6 @@ futures = "0.3"
itertools = "0.13.0" itertools = "0.13.0"
log = "0.4" log = "0.4"
oneshot = "0.1.10" oneshot = "0.1.10"
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }
flume = { version = "0.11.1", default-features = false, features = ["async", "select"] } flume = { version = "0.11.1", default-features = false, features = ["async", "select"] }
rust-embed = "8.5.0" rust-embed = "8.5.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }

View File

@@ -9,7 +9,6 @@ common = { path = "../common" }
gpui.workspace = true gpui.workspace = true
gpui_tokio.workspace = true gpui_tokio.workspace = true
reqwest.workspace = true
anyhow.workspace = true anyhow.workspace = true
smol.workspace = true smol.workspace = true
log.workspace = true log.workspace = true

View File

@@ -7,11 +7,13 @@ use anyhow::{anyhow, Context as AnyhowContext, Error};
use gpui::http_client::{AsyncBody, HttpClient}; use gpui::http_client::{AsyncBody, HttpClient};
use gpui::{ use gpui::{
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task, App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
Window,
}; };
use semver::Version; use semver::Version;
use serde::Deserialize; use serde::Deserialize;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs::File; use smol::fs::File;
use smol::io::AsyncReadExt;
use smol::process::Command; use smol::process::Command;
const GITHUB_API_URL: &str = "https://api.github.com"; const GITHUB_API_URL: &str = "https://api.github.com";
@@ -30,14 +32,14 @@ fn is_flatpak_installation() -> bool {
std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok() std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok()
} }
pub fn init(cx: &mut App) { pub fn init(window: &mut Window, cx: &mut App) {
// Skip auto-update initialization if installed via Flatpak // Skip auto-update initialization if installed via Flatpak
if is_flatpak_installation() { if is_flatpak_installation() {
log::info!("Skipping auto-update initialization: App is installed via Flatpak"); log::info!("Skipping auto-update initialization: App is installed via Flatpak");
return; return;
} }
AutoUpdater::set_global(cx.new(AutoUpdater::new), cx); AutoUpdater::set_global(cx.new(|cx| AutoUpdater::new(window, cx)), cx);
} }
struct GlobalAutoUpdater(Entity<AutoUpdater>); struct GlobalAutoUpdater(Entity<AutoUpdater>);
@@ -181,7 +183,7 @@ pub struct AutoUpdater {
_subscriptions: SmallVec<[Subscription; 1]>, _subscriptions: SmallVec<[Subscription; 1]>,
/// Background tasks /// Background tasks
_tasks: SmallVec<[Task<()>; 2]>, tasks: Vec<Task<Result<(), Error>>>,
} }
impl AutoUpdater { impl AutoUpdater {
@@ -195,44 +197,9 @@ impl AutoUpdater {
cx.set_global(GlobalAutoUpdater(state)); cx.set_global(GlobalAutoUpdater(state));
} }
fn new(cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap(); let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
let async_version = version.clone();
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
let mut tasks = smallvec![];
tasks.push(
// Check for updates after 2 minutes
cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_secs(120))
.await;
// Update the status to checking
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Checking, cx);
})
.ok();
match Self::check_for_updates(async_version, cx).await {
Ok(download_url) => {
// Update the status to checked with download URL
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::checked(download_url), cx);
})
.ok();
}
Err(e) => {
log::warn!("Failed to check for updates: {e}");
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Idle, cx);
})
.ok();
}
}
}),
);
subscriptions.push( subscriptions.push(
// Observe the status // Observe the status
@@ -243,11 +210,16 @@ impl AutoUpdater {
}), }),
); );
// Run at the end of current cycle
cx.defer_in(window, |this, _window, cx| {
this.check(cx);
});
Self { Self {
status: AutoUpdateStatus::Idle, status: AutoUpdateStatus::Idle,
version, version,
tasks: vec![],
_subscriptions: subscriptions, _subscriptions: subscriptions,
_tasks: tasks,
} }
} }
@@ -256,31 +228,63 @@ impl AutoUpdater {
cx.notify(); cx.notify();
} }
fn check_for_updates(version: Version, cx: &AsyncApp) -> Task<Result<String, Error>> { fn check(&mut self, cx: &mut Context<Self>) {
let version = self.version.clone();
let duration = Duration::from_secs(120);
let task = self.check_for_updates(version, cx);
// Check for updates after 2 minutes
self.tasks.push(cx.spawn(async move |this, cx| {
cx.background_executor().timer(duration).await;
// Update the status to checking
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Checking, cx);
})?;
match task.await {
Ok(download_url) => {
// Update the status to checked with download URL
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::checked(download_url), cx);
})?;
}
Err(e) => {
log::warn!("Failed to check for updates: {e}");
this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Idle, cx);
})?;
}
}
Ok(())
}));
}
fn check_for_updates(&self, version: Version, cx: &App) -> Task<Result<String, Error>> {
let http_client = cx.http_client();
let repo_owner = get_github_repo_owner();
let repo_name = get_github_repo_name();
cx.background_spawn(async move { cx.background_spawn(async move {
let client = reqwest::Client::new();
let repo_owner = get_github_repo_owner();
let repo_name = get_github_repo_name();
let url = format!( let url = format!(
"{}/repos/{}/{}/releases/latest", "{}/repos/{}/{}/releases/latest",
GITHUB_API_URL, repo_owner, repo_name GITHUB_API_URL, repo_owner, repo_name
); );
let response = client let async_body = AsyncBody::default();
.get(&url) let mut body = Vec::new();
.header("User-Agent", "Coop-Auto-Updater") let mut response = http_client.get(&url, async_body, false).await?;
.send()
.await // Read the response body into a vector
.context("Failed to fetch GitHub releases")?; response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() { if !response.status().is_success() {
return Err(anyhow!("GitHub API returned error: {}", response.status())); return Err(anyhow!("GitHub API returned error: {}", response.status()));
} }
let release: GitHubRelease = response // Parse the response body as JSON
.json() let release: GitHubRelease = serde_json::from_slice(&body)?;
.await
.context("Failed to parse GitHub release")?;
// Parse version from tag (remove 'v' prefix if present) // Parse version from tag (remove 'v' prefix if present)
let tag_version = release.tag_name.trim_start_matches('v'); let tag_version = release.tag_name.trim_start_matches('v');
@@ -334,29 +338,31 @@ impl AutoUpdater {
Ok((installer_dir, target_path)) Ok((installer_dir, target_path))
}); });
self._tasks.push( self.tasks.push(
// Install the new release // Install the new release
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
_ = this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Installing, cx); this.set_status(AutoUpdateStatus::Installing, cx);
}); })?;
match task.await { match task.await {
Ok((installer_dir, target_path)) => { Ok((installer_dir, target_path)) => {
if Self::install(installer_dir, target_path, cx).await.is_ok() { if Self::install(installer_dir, target_path, cx).await.is_ok() {
// Update the status to updated // Update the status to updated
_ = this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::Updated, cx); this.set_status(AutoUpdateStatus::Updated, cx);
}); })?;
} }
} }
Err(e) => { Err(e) => {
// Update the status to error including the error message // Update the status to error including the error message
_ = this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_status(AutoUpdateStatus::error(e.to_string()), cx); this.set_status(AutoUpdateStatus::error(e.to_string()), cx);
}); })?;
} }
} }
Ok(())
}), }),
); );
} }

View File

@@ -4,7 +4,7 @@ use std::sync::Arc;
pub use actions::*; pub use actions::*;
use anyhow::{Context as AnyhowContext, Error}; use anyhow::{Context as AnyhowContext, Error};
use chat::{Message, RenderedMessage, Room, RoomEvent, SendReport}; use chat::{Message, RenderedMessage, Room, RoomEvent, SendReport};
use common::{nip96_upload, RenderedTimestamp}; use common::RenderedTimestamp;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
deferred, div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext, deferred, div, img, list, px, red, relative, rems, svg, white, AnyElement, App, AppContext,
@@ -21,7 +21,7 @@ use settings::AppSettings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::fs; use smol::fs;
use smol::lock::RwLock; use smol::lock::RwLock;
use state::NostrRegistry; use state::{nostr_upload, NostrRegistry};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -504,7 +504,7 @@ impl ChatPanel {
let upload = Tokio::spawn(cx, async move { let upload = Tokio::spawn(cx, async move {
let file = fs::read(path).await.ok()?; let file = fs::read(path).await.ok()?;
let url = nip96_upload(&client, &nip96_server, file).await.ok()?; let url = nostr_upload(&client, &nip96_server, file).await.ok()?;
Some(url) Some(url)
}); });

View File

@@ -15,7 +15,6 @@ chrono.workspace = true
smallvec.workspace = true smallvec.workspace = true
smol.workspace = true smol.workspace = true
futures.workspace = true futures.workspace = true
reqwest.workspace = true
log.workspace = true log.workspace = true
dirs = "5.0" dirs = "5.0"

View File

@@ -1,11 +1,9 @@
pub use debounced_delay::*; pub use debounced_delay::*;
pub use display::*; pub use display::*;
pub use event::*; pub use event::*;
pub use nip96::*;
pub use paths::*; pub use paths::*;
mod debounced_delay; mod debounced_delay;
mod display; mod display;
mod event; mod event;
mod nip96;
mod paths; mod paths;

View File

@@ -1,31 +0,0 @@
use anyhow::anyhow;
use nostr::prelude::*;
use reqwest::Client as ReqClient;
pub async fn nip05_verify(public_key: PublicKey, address: &str) -> Result<bool, anyhow::Error> {
let req_client = ReqClient::new();
let address = Nip05Address::parse(address)?;
// Get NIP-05 response
let res = req_client.get(address.url().to_string()).send().await?;
let json: Value = res.json().await?;
let verify = nip05::verify_from_json(&public_key, &address, &json);
Ok(verify)
}
pub async fn nip05_profile(address: &str) -> Result<Nip05Profile, anyhow::Error> {
let req_client = ReqClient::new();
let address = Nip05Address::parse(address)?;
// Get NIP-05 response
let res = req_client.get(address.url().to_string()).send().await?;
let json: Value = res.json().await?;
if let Ok(profile) = Nip05Profile::from_json(&address, &json) {
Ok(profile)
} else {
Err(anyhow!("Failed to get NIP-05 profile"))
}
}

View File

@@ -44,6 +44,9 @@ relay_auth = { path = "../relay_auth" }
gpui.workspace = true gpui.workspace = true
gpui_platform.workspace = true gpui_platform.workspace = true
gpui_linux.workspace = true
gpui_windows.workspace = true
gpui_macos.workspace = true
gpui_tokio.workspace = true gpui_tokio.workspace = true
reqwest_client.workspace = true reqwest_client.workspace = true

View File

@@ -103,7 +103,7 @@ fn main() {
person::init(cx); person::init(cx);
// Initialize auto update // Initialize auto update
auto_update::init(cx); auto_update::init(window, cx);
// Root Entity // Root Entity
Root::new(workspace::init(window, cx).into(), window, cx) Root::new(workspace::init(window, cx).into(), window, cx)

View File

@@ -2,7 +2,7 @@ use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use common::{nip96_upload, shorten_pubkey}; use common::shorten_pubkey;
use gpui::{ use gpui::{
div, rems, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, div, rems, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter,
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
@@ -13,7 +13,7 @@ use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry}; use person::{Person, PersonRegistry};
use settings::AppSettings; use settings::AppSettings;
use smol::fs; use smol::fs;
use state::NostrRegistry; use state::{nostr_upload, NostrRegistry};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
@@ -170,7 +170,7 @@ impl ProfilePanel {
Ok(Ok(Some(mut paths))) => { Ok(Ok(Some(mut paths))) => {
if let Some(path) = paths.pop() { if let Some(path) = paths.pop() {
let file = fs::read(path).await?; let file = fs::read(path).await?;
let url = nip96_upload(&client, &nip96_server, file).await?; let url = nostr_upload(&client, &nip96_server, file).await?;
Ok(url) Ok(url)
} else { } else {

View File

@@ -7,6 +7,7 @@ publish.workspace = true
[dependencies] [dependencies]
common = { path = "../common" } common = { path = "../common" }
nostr.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true
nostr-lmdb.workspace = true nostr-lmdb.workspace = true
nostr-connect.workspace = true nostr-connect.workspace = true
@@ -14,7 +15,6 @@ nostr-connect.workspace = true
gpui.workspace = true gpui.workspace = true
gpui_tokio.workspace = true gpui_tokio.workspace = true
smol.workspace = true smol.workspace = true
reqwest.workspace = true
flume.workspace = true flume.workspace = true
log.workspace = true log.workspace = true
anyhow.workspace = true anyhow.workspace = true
@@ -25,3 +25,4 @@ serde_json.workspace = true
rustls = "0.23" rustls = "0.23"
petname = "2.0.2" petname = "2.0.2"
whoami = "1.6.1" whoami = "1.6.1"
reqwest = { version = "0.12", features = ["multipart", "stream", "json"] }

View File

@@ -13,11 +13,13 @@ use nostr_sdk::prelude::*;
mod constants; mod constants;
mod gossip; mod gossip;
mod nip05; mod nip05;
mod nip96;
mod signer; mod signer;
pub use constants::*; pub use constants::*;
pub use gossip::*; pub use gossip::*;
pub use nip05::*; pub use nip05::*;
pub use nip96::*;
pub use signer::*; pub use signer::*;
pub fn init(window: &mut Window, cx: &mut App) { pub fn init(window: &mut Window, cx: &mut App) {

View File

@@ -59,7 +59,7 @@ where
Ok(upload_response.download_url()?.to_owned()) Ok(upload_response.download_url()?.to_owned())
} }
pub async fn nip96_upload( pub async fn nostr_upload(
client: &Client, client: &Client,
server: &Url, server: &Url,
file: Vec<u8>, file: Vec<u8>,

View File

@@ -78,6 +78,9 @@ pub struct Theme {
/// Show the scrollbar mode, default: scrolling /// Show the scrollbar mode, default: scrolling
pub scrollbar_mode: ScrollbarMode, pub scrollbar_mode: ScrollbarMode,
/// Platform kind
pub platform: PlatformKind,
} }
impl Deref for Theme { impl Deref for Theme {
@@ -201,6 +204,7 @@ impl From<ThemeFamily> for Theme {
mode, mode,
colors: *colors, colors: *colors,
theme: Rc::new(family), theme: Rc::new(family),
platform,
} }
} }
} }

View File

@@ -1,12 +1,11 @@
use std::mem;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use gpui::MouseButton; use gpui::MouseButton;
#[cfg(not(target_os = "windows"))]
use gpui::Pixels;
use gpui::{ use gpui::{
div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement, div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
ParentElement, Pixels, Render, StatefulInteractiveElement as _, Styled, Window, ParentElement, Render, StatefulInteractiveElement as _, Styled, Window, WindowControlArea,
WindowControlArea,
}; };
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING}; use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING};
@@ -14,42 +13,39 @@ use ui::h_flex;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use crate::platforms::linux::LinuxWindowControls; use crate::platforms::linux::LinuxWindowControls;
use crate::platforms::mac::TRAFFIC_LIGHT_PADDING;
use crate::platforms::windows::WindowsWindowControls; use crate::platforms::windows::WindowsWindowControls;
mod platforms; mod platforms;
/// Titlebar
pub struct TitleBar { pub struct TitleBar {
/// Children elements of the title bar.
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
platform_kind: PlatformKind,
should_move: bool,
}
impl Default for TitleBar { /// Whether the title bar is currently being moved.
fn default() -> Self { should_move: bool,
Self::new()
}
} }
impl TitleBar { impl TitleBar {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
children: smallvec![], children: smallvec![],
platform_kind: PlatformKind::platform(),
should_move: false, should_move: false,
} }
} }
#[cfg(not(target_os = "windows"))] #[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.)) (1.75 * window.rem_size()).max(px(34.))
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn height(_window: &mut Window) -> Pixels { pub fn height(&self, _window: &mut Window) -> Pixels {
px(32.) 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 cfg!(any(target_os = "linux", target_os = "freebsd")) {
if window.is_window_active() && !self.should_move { if window.is_window_active() && !self.should_move {
cx.theme().title_bar cx.theme().title_bar
@@ -69,66 +65,62 @@ impl TitleBar {
} }
} }
impl ParentElement for TitleBar { impl Default for TitleBar {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) { fn default() -> Self {
self.children.extend(elements) Self::new()
} }
} }
impl Render for TitleBar { impl Render for TitleBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let height = self.height(window);
let color = self.titlebar_color(window, cx);
let children = std::mem::take(&mut self.children);
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let supported_controls = window.window_controls(); let supported_controls = window.window_controls();
let decorations = window.window_decorations(); 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() h_flex()
.window_control_area(WindowControlArea::Drag) .window_control_area(WindowControlArea::Drag)
.w_full()
.h(height) .h(height)
.w_full()
.map(|this| { .map(|this| {
if window.is_fullscreen() { if window.is_fullscreen() {
this.px_2() this.px_2()
} else if self.platform_kind.is_mac() { } else if cx.theme().platform.is_mac() {
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING)) this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING))
.pr_2()
.when(children.len() <= 1, |this| {
this.pr(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
})
} else { } else {
this.px_2() this.px_2()
} }
}) })
.map(|this| match decorations { .map(|this| match decorations {
Decorations::Server => this, Decorations::Server => this,
Decorations::Client { tiling, .. } => this Decorations::Client { tiling } => this
.when(!(tiling.top || tiling.right), |el| { .when(!(tiling.top || tiling.right), |div| {
el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING) div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
}) })
.when(!(tiling.top || tiling.left), |el| { .when(!(tiling.top || tiling.left), |div| {
el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING) div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
}), }),
}) })
.bg(color) .bg(color)
.border_b_1()
.border_color(cx.theme().border)
.content_stretch() .content_stretch()
.child( .child(
div() h_flex()
.id("title-bar") .id("title-bar")
.flex()
.flex_row()
.items_center()
.justify_between() .justify_between()
.w_full() .w_full()
.when(self.platform_kind.is_mac(), |this| { .when(cx.theme().platform.is_mac(), |this| {
this.on_click(|event, window, _| { this.on_click(|event, window, _| {
if event.click_count() == 2 { if event.click_count() == 2 {
window.titlebar_double_click(); window.titlebar_double_click();
} }
}) })
}) })
.when(self.platform_kind.is_linux(), |this| { .when(cx.theme().platform.is_linux(), |this| {
this.on_click(|event, window, _| { this.on_click(|event, window, _| {
if event.click_count() == 2 { if event.click_count() == 2 {
window.zoom_window(); window.zoom_window();
@@ -137,45 +129,60 @@ impl Render for TitleBar {
}) })
.children(children), .children(children),
) )
.when(!window.is_fullscreen(), |this| match self.platform_kind { .child(
PlatformKind::Linux => { h_flex()
#[cfg(target_os = "linux")] .absolute()
if matches!(decorations, Decorations::Client { .. }) { .top_0()
this.child(LinuxWindowControls::new(None)) .right_0()
.when(supported_controls.window_menu, |this| { .pr_2()
this.on_mouse_down(MouseButton::Right, move |ev, window, _| { .h(height)
window.show_window_menu(ev.position) .child(
}) div().when(!window.is_fullscreen(), |this| match cx.theme().platform {
}) PlatformKind::Linux => {
.on_mouse_move(cx.listener(move |this, _ev, window, _| { #[cfg(target_os = "linux")]
if this.should_move { if matches!(decorations, Decorations::Client { .. }) {
this.should_move = false; this.child(LinuxWindowControls::new(None))
window.start_window_move(); .when(supported_controls.window_menu, |this| {
this.on_mouse_down(
MouseButton::Right,
move |ev, window, _| {
window.show_window_menu(ev.position)
},
)
})
.on_mouse_move(cx.listener(move |this, _ev, window, _| {
if this.should_move {
this.should_move = false;
window.start_window_move();
}
}))
.on_mouse_down_out(cx.listener(
move |this, _ev, _window, _cx| {
this.should_move = false;
},
))
.on_mouse_up(
MouseButton::Left,
cx.listener(move |this, _ev, _window, _cx| {
this.should_move = false;
}),
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _ev, _window, _cx| {
this.should_move = true;
}),
)
} else {
this
} }
})) #[cfg(not(target_os = "linux"))]
.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| { this
this.should_move = false; }
})) PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
.on_mouse_up( PlatformKind::Mac => this,
MouseButton::Left, }),
cx.listener(move |this, _ev, _window, _cx| { ),
this.should_move = false; )
}),
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, _ev, _window, _cx| {
this.should_move = true;
}),
)
} else {
this
}
#[cfg(not(target_os = "linux"))]
this
}
PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
PlatformKind::Mac => this,
})
} }
} }

View File

@@ -1,11 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::OnceLock; use std::sync::OnceLock;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
img, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, svg, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
StatefulInteractiveElement, Styled, Window, SharedString, StatefulInteractiveElement, Styled, Window,
}; };
use linicon::{lookup_icon, IconType}; use linicon::{lookup_icon, IconType};
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -26,21 +25,26 @@ impl LinuxWindowControls {
impl RenderOnce for LinuxWindowControls { impl RenderOnce for LinuxWindowControls {
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
let supported_controls = window.window_controls();
h_flex() h_flex()
.id("linux-window-controls") .id("linux-window-controls")
.px_2()
.gap_2() .gap_2()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.child(WindowControl::new( .when(supported_controls.minimize, |this| {
LinuxControl::Minimize, this.child(WindowControl::new(
IconName::WindowMinimize, LinuxControl::Minimize,
)) IconName::WindowMinimize,
.child({ ))
if window.is_maximized() { })
WindowControl::new(LinuxControl::Restore, IconName::WindowRestore) .when(supported_controls.maximize, |this| {
} else { this.child({
WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize) if window.is_maximized() {
} WindowControl::new(LinuxControl::Restore, IconName::WindowRestore)
} else {
WindowControl::new(LinuxControl::Maximize, IconName::WindowMaximize)
}
})
}) })
.child( .child(
WindowControl::new(LinuxControl::Close, IconName::WindowClose) WindowControl::new(LinuxControl::Close, IconName::WindowClose)
@@ -87,24 +91,22 @@ impl RenderOnce for WindowControl {
.justify_center() .justify_center()
.items_center() .items_center()
.rounded_full() .rounded_full()
.map(|this| { .size_6()
if is_gnome { .when(is_gnome, |this| {
this.size_6() this.bg(cx.theme().ghost_element_background_alt)
.bg(cx.theme().tab_inactive_background) .hover(|this| this.bg(cx.theme().ghost_element_hover))
.hover(|this| this.bg(cx.theme().tab_hover_background)) .active(|this| this.bg(cx.theme().ghost_element_active))
.active(|this| this.bg(cx.theme().tab_active_background))
} else {
this.size_5()
.bg(cx.theme().ghost_element_background)
.hover(|this| this.bg(cx.theme().ghost_element_hover))
.active(|this| this.bg(cx.theme().ghost_element_active))
}
}) })
.map(|this| { .map(|this| {
if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() { if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() {
this.child(img(path).flex_grow().size_4()) this.child(
svg()
.external_path(SharedString::from(path))
.size_4()
.text_color(cx.theme().text),
)
} else { } else {
this.child(Icon::new(self.fallback).flex_grow().small()) this.child(Icon::new(self.fallback).small().text_color(cx.theme().text))
} }
}) })
.on_mouse_move(|_, _window, cx| cx.stop_propagation()) .on_mouse_move(|_, _window, cx| cx.stop_propagation())
@@ -114,20 +116,14 @@ impl RenderOnce for WindowControl {
LinuxControl::Minimize => window.minimize_window(), LinuxControl::Minimize => window.minimize_window(),
LinuxControl::Restore => window.zoom_window(), LinuxControl::Restore => window.zoom_window(),
LinuxControl::Maximize => window.zoom_window(), LinuxControl::Maximize => window.zoom_window(),
LinuxControl::Close => window.dispatch_action( LinuxControl::Close => cx.quit(),
self.close_action
.as_ref()
.expect("Use WindowControl::new_close() for close control.")
.boxed_clone(),
cx,
),
} }
}) })
} }
} }
static DE: OnceLock<DesktopEnvironment> = OnceLock::new(); static DE: OnceLock<DesktopEnvironment> = OnceLock::new();
static LINUX_CONTROLS: OnceLock<HashMap<LinuxControl, Option<PathBuf>>> = OnceLock::new(); static LINUX_CONTROLS: OnceLock<HashMap<LinuxControl, Option<String>>> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum DesktopEnvironment { pub enum DesktopEnvironment {
@@ -182,7 +178,7 @@ impl LinuxControl {
} }
} }
fn linux_controls() -> &'static HashMap<LinuxControl, Option<PathBuf>> { fn linux_controls() -> &'static HashMap<LinuxControl, Option<String>> {
LINUX_CONTROLS.get_or_init(|| { LINUX_CONTROLS.get_or_init(|| {
let mut icons = HashMap::new(); let mut icons = HashMap::new();
icons.insert(LinuxControl::Close, None); icons.insert(LinuxControl::Close, None);
@@ -219,7 +215,9 @@ fn linux_controls() -> &'static HashMap<LinuxControl, Option<PathBuf>> {
} }
if let Some(Ok(icon)) = control_icon { if let Some(Ok(icon)) = control_icon {
icons.entry(control).and_modify(|v| *v = Some(icon.path)); icons
.entry(control)
.and_modify(|v| *v = Some(icon.path.to_string_lossy().to_string()));
} }
} }
} }

View File

@@ -1,5 +1,5 @@
[toolchain] [toolchain]
channel = "1.91" channel = "1.92"
profile = "minimal" profile = "minimal"
components = ["rustfmt", "clippy"] components = ["rustfmt", "clippy"]
targets = [ targets = [