diff --git a/Cargo.lock b/Cargo.lock index 988834fa50cb4b402acb26104e964cce848d1e23..aaa953d50fe3962d3a502342890caa1bae42c25f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7584,6 +7584,7 @@ dependencies = [ "gpui", "gpui_wgpu", "http_client", + "image", "itertools 0.14.0", "libc", "log", diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index efca26a6b4802037a96490bf81f7d1c5c1d8b298..5028915c968d07994c7496728d7a71ad593fe502 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1424,6 +1424,9 @@ pub struct WindowOptions { /// Note that this may be ignored. pub window_decorations: Option, + /// Icon image (X11 only) + pub icon: Option>, + /// Tab group name, allows opening the window as a native tab on macOS 10.12+. Windows with the same tabbing identifier will be grouped together. pub tabbing_identifier: Option, } @@ -1470,6 +1473,10 @@ pub struct WindowParams { #[cfg_attr(any(target_os = "linux", target_os = "freebsd"), allow(dead_code))] pub show: bool, + /// An image to set as the window icon (x11 only) + #[cfg_attr(feature = "wayland", allow(dead_code))] + pub icon: Option>, + #[cfg_attr(feature = "wayland", allow(dead_code))] pub display_id: Option, @@ -1530,6 +1537,7 @@ impl Default for WindowOptions { is_minimizable: true, display_id: None, window_background: WindowBackgroundAppearance::default(), + icon: None, app_id: None, window_min_size: None, window_decorations: None, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index dc357bda80f4329a1ae5b9894ea329c44e483475..86e48def36e73f592d5e9c019c4d89d2fd8fae42 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1133,6 +1133,11 @@ impl Window { app_id, window_min_size, window_decorations, + #[cfg_attr( + not(any(target_os = "linux", target_os = "freebsd")), + allow(unused_variables) + )] + icon, #[cfg_attr(not(target_os = "macos"), allow(unused_variables))] tabbing_identifier, } = options; @@ -1151,6 +1156,7 @@ impl Window { show, display_id, window_min_size, + icon, #[cfg(target_os = "macos")] tabbing_identifier, }, diff --git a/crates/gpui_linux/Cargo.toml b/crates/gpui_linux/Cargo.toml index 9078fa82c2884421c6cd11c6d3384645621b7e6f..a088a5205fd8c23a251c2f93cc563de144991e44 100644 --- a/crates/gpui_linux/Cargo.toml +++ b/crates/gpui_linux/Cargo.toml @@ -54,6 +54,7 @@ screen-capture = [ anyhow.workspace = true bytemuck = "1" collections.workspace = true +image.workspace = true futures.workspace = true gpui.workspace = true gpui_wgpu = { workspace = true, optional = true, features = ["font-kit"] } diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index f29ba49fb2498dd49a5f025aad4dc2584a8a8a42..c21d8baf31de06e6fe155678c5243c21ec6cba53 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -60,6 +60,7 @@ x11rb::atom_manager! { WM_TRANSIENT_FOR, _NET_WM_PID, _NET_WM_NAME, + _NET_WM_ICON, _NET_WM_STATE, _NET_WM_STATE_MAXIMIZED_VERT, _NET_WM_STATE_MAXIMIZED_HORZ, @@ -743,6 +744,29 @@ impl X11WindowState { size_hints.set_normal_hints(xcb, x_window), )?; + if let Some(image) = params.icon { + // https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html#id-1.6.13 + let property_size = 2 + (image.width() * image.height()) as usize; + let mut property_data: Vec = Vec::with_capacity(property_size); + property_data.push(image.width()); + property_data.push(image.height()); + property_data.extend(image.pixels().map(|px| { + let [r, g, b, a]: [u8; 4] = px.0; + u32::from_le_bytes([b, g, r, a]) + })); + + check_reply( + || "X11 ChangeProperty32 for _NET_ICON_NAME failed.", + xcb.change_property32( + xproto::PropMode::REPLACE, + x_window, + atoms._NET_WM_ICON, + xproto::AtomEnum::CARDINAL, + &property_data, + ), + )?; + } + let display = Rc::new(X11Display::new(xcb, scale_factor, x_screen_index)?); Ok(Self { diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 8811a4159a0f539d2bae2c62242a3d5f490686ef..7ce5badb2baa967b613d7298cb4bcbc6584fe3cc 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -628,6 +628,7 @@ impl MacWindow { display_id, window_min_size, tabbing_identifier, + .. }: WindowParams, foreground_executor: ForegroundExecutor, background_executor: BackgroundExecutor, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 915748f50bf75c63ff8f658581d2f247e2d2e44b..1632f9f925c38bd7740aabcab27eeb901607b14b 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -240,6 +240,11 @@ gpui = { workspace = true, features = [ "x11", ] } ashpd.workspace = true +image.workspace = true + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies] +image.workspace = true + [target.'cfg(target_os = "linux")'.build-dependencies] pkg-config = "0.3.22" @@ -262,6 +267,8 @@ agent_ui = { workspace = true, features = ["test-support"] } search = { workspace = true, features = ["test-support"] } repl = { workspace = true, features = ["test-support"] } + + [package.metadata.bundle-dev] icon = ["resources/app-icon-dev@2x.png", "resources/app-icon-dev.png"] identifier = "dev.zed.Zed-Dev" diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 4b27d939aee833058d03771907b53324a6ce50d0..80bf1d8642e253cb2e0dfb1389a33af5cb1b516b 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -235,4 +235,50 @@ fn main() { } } } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + prepare_app_icon_x11(); +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +fn icon_path() -> std::path::PathBuf { + use std::str::FromStr; + + let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("dev"); + let channel = match release_channel { + "stable" => "", + "preview" => "-preview", + "nightly" => "-nightly", + "dev" => "-dev", + _ => "-dev", + }; + + #[cfg(windows)] + let icon = format!("resources/windows/app-icon{}.ico", channel); + #[cfg(not(windows))] + let icon = format!("resources/app-icon{}.png", channel); + + std::path::PathBuf::from_str(&icon).unwrap() +} + +#[cfg(any(target_os = "linux", target_os = "freebsd"))] +fn prepare_app_icon_x11() { + use image::{ImageReader, imageops}; + use std::env; + use std::path::Path; + + let out_dir = env::var("OUT_DIR").unwrap(); + + let resized_image = ImageReader::open(icon_path()) + .unwrap() + .decode() + .unwrap() + .resize(256, 256, imageops::FilterType::Lanczos3); + + // name should match include_bytes! call in src/zed.rs + let icon_out_path = Path::new(&out_dir).join("app_icon.png"); + resized_image.save(&icon_out_path).expect("saving app icon"); + + println!("cargo:rerun-if-env-changed=RELEASE_CHANNEL"); + println!("cargo:rerun-if-changed={}", icon_path().to_string_lossy()); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fd28c268e0a753cb41661aab5a9ac457b2518f89..bb4a8dc2950dd397fd71576c7da4d078bc1d43c7 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -324,6 +324,21 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + static APP_ICON: std::sync::LazyLock>> = + std::sync::LazyLock::new(|| { + // this shouldn't fail since decode is checked in build.rs + const BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/app_icon.png")); + util::maybe!({ + let image = image::ImageReader::new(std::io::Cursor::new(BYTES)) + .with_guessed_format()? + .decode()? + .into(); + anyhow::Ok(Arc::new(image)) + }) + .log_err() + }); + WindowOptions { titlebar: Some(TitlebarOptions { title: None, @@ -338,6 +353,8 @@ pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowO display_id: display.map(|display| display.id()), window_background: cx.theme().window_background_appearance(), app_id: Some(app_id.to_owned()), + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + icon: APP_ICON.as_ref().cloned(), window_decorations: Some(window_decorations), window_min_size: Some(gpui::Size { width: px(360.0),