diff --git a/Cargo.lock b/Cargo.lock index 85d3f9ac3a68aa8a420910dfa61102f03c9812ef..d5b3281dbb62e6bfce8e7914d469e8815a7863e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7592,6 +7592,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 5778d6ac7372f4b13f14d4fa7d0ebca54a03fd1d..44990c8c9ddb423aaa030f5161046dd07006d1d5 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,8 @@ impl Window { show, display_id, window_min_size, + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + 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 1974cc0bb28f62da4d7dcb3e9fca92b6324470bb..0607283f04e71de312bf8212879f87daf453f7f4 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, @@ -740,6 +741,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/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..c741a974d7ab10e07b8245540ce71521ef114f09 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -1,4 +1,5 @@ #![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] +use std::path::Path; use std::process::Command; fn main() { @@ -202,18 +203,8 @@ fn main() { } } - let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("dev"); - let icon = match release_channel { - "stable" => "resources/windows/app-icon.ico", - "preview" => "resources/windows/app-icon-preview.ico", - "nightly" => "resources/windows/app-icon-nightly.ico", - "dev" => "resources/windows/app-icon-dev.ico", - _ => "resources/windows/app-icon-dev.ico", - }; - let icon = std::path::Path::new(icon); - println!("cargo:rerun-if-env-changed=RELEASE_CHANNEL"); - println!("cargo:rerun-if-changed={}", icon.display()); + println!("cargo:rerun-if-changed={}", icon_path().display()); #[cfg(windows)] { @@ -225,7 +216,7 @@ fn main() { if let Some(explicit_rc_toolkit_path) = std::env::var("ZED_RC_TOOLKIT_PATH").ok() { res.set_toolkit_path(explicit_rc_toolkit_path.as_str()); } - res.set_icon(icon.to_str().unwrap()); + res.set_icon(icon_path().to_str().unwrap()); res.set("FileDescription", "Zed"); res.set("ProductName", "Zed"); @@ -235,4 +226,53 @@ fn main() { } } } + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + prepare_app_icon_x11(); +} + +fn icon_path() -> &'static Path { + let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("dev"); + let icon = match release_channel { + "stable" => "resources/app-icon.png", + "preview" => "resources/app-icon-preview.png", + "nightly" => "resources/app-icon-nightly.png", + "dev" | _ => "resources/app-icon-dev.png", + }; + + Path::new(icon) +} + +fn prepare_app_icon_x11() { + use image::{DynamicImage, ImageReader, ImageResult, imageops}; + use std::env; + use std::path::Path; + + let out_dir = env::var("OUT_DIR").unwrap(); + + let resized_image = + match || -> ImageResult { Ok(ImageReader::open(icon_path())?.decode()?) }() { + Err(msg) => { + eprintln!("failed to read or decode {}: {msg}", icon_path().display()); + std::process::exit(1); + } + Ok(image) => imageops::resize(&image, 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"); + + // verify icon can be read and decoded + if let Err(msg) = ImageReader::open(&icon_out_path).unwrap().decode() { + eprintln!( + "error verifying {}: {msg} (resized from {})", + icon_out_path.display(), + icon_path().display(), + ); + std::process::exit(1); + } + + 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 3d4ada8a1b90020090eb74a8a6ea752fa7a44ab3..66aa2e5cc773c4d5a67798b4a7ed254f44c5a50c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -325,6 +325,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, @@ -339,6 +354,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),