Cargo.lock 🔗
@@ -7584,6 +7584,7 @@ dependencies = [
"gpui",
"gpui_wgpu",
"http_client",
+ "image",
"itertools 0.14.0",
"libc",
"log",
kitt and Yara created
Closes #30644
Many X11 environments expect a window icon to be supplied [as pixel data
on a window property
`_NET_WM_ICON`](https://specifications.freedesktop.org/wm-spec/1.4/ar01s05.html#id-1.6.13).
I confirmed this change fixes the icon in xfce4 for me, I think its
likely it also fixes https://github.com/zed-industries/zed/issues/37961
but I haven't tested it.
## Questions
* [`image::RgbaImage` is exposed to the public API of
gpui](https://github.com/zed-industries/zed/pull/40096/files#diff-318f166d72ad9476bd0a116446f5db3897fc1a4eb1d49aaf8105608bcf49ea53R1136).
I would guess this is undesirable, but I wasn't sure of the best way to
use gpui's native `Image` type..
* Currently [the icon is embedded into the
binary](https://github.com/zed-industries/zed/pull/40096/files#diff-89af0b4072205c53b518aa977d6be48997e1a51fa4dbf06c7ddd1fec99fc510eR101).
If this is undesirable, zed could alternatively implement [icon
lookup](https://specifications.freedesktop.org/icon-theme-spec/latest/#icon_lookup)
and try and find its icon from the system at runtime.
## Future work
* It might be nice to expose a `set_window_icon` method also (it could
be used for example to show dirty state in the icon somehow), but I'm
unfamiliar with what other platforms support and if this could be beyond
X11 (there is a [wayland
protocol](https://wayland.app/protocols/xdg-toplevel-icon-v1) though!).
Release Notes:
- Fixed missing window icon on X11
---------
Co-authored-by: Yara <git@yara.blue>
Cargo.lock | 1
crates/gpui/src/platform.rs | 8 ++++
crates/gpui/src/window.rs | 6 +++
crates/gpui_linux/Cargo.toml | 1
crates/gpui_linux/src/linux/x11/window.rs | 24 +++++++++++++
crates/gpui_macos/src/window.rs | 1
crates/zed/Cargo.toml | 7 +++
crates/zed/build.rs | 46 +++++++++++++++++++++++++
crates/zed/src/zed.rs | 17 +++++++++
9 files changed, 111 insertions(+)
@@ -7584,6 +7584,7 @@ dependencies = [
"gpui",
"gpui_wgpu",
"http_client",
+ "image",
"itertools 0.14.0",
"libc",
"log",
@@ -1424,6 +1424,9 @@ pub struct WindowOptions {
/// Note that this may be ignored.
pub window_decorations: Option<WindowDecorations>,
+ /// Icon image (X11 only)
+ pub icon: Option<Arc<image::RgbaImage>>,
+
/// 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<String>,
}
@@ -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<Arc<image::RgbaImage>>,
+
#[cfg_attr(feature = "wayland", allow(dead_code))]
pub display_id: Option<DisplayId>,
@@ -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,
@@ -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,
},
@@ -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"] }
@@ -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<u32> = 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 {
@@ -628,6 +628,7 @@ impl MacWindow {
display_id,
window_min_size,
tabbing_identifier,
+ ..
}: WindowParams,
foreground_executor: ForegroundExecutor,
background_executor: BackgroundExecutor,
@@ -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"
@@ -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());
}
@@ -324,6 +324,21 @@ pub fn build_window_options(display_uuid: Option<Uuid>, 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<Option<std::sync::Arc<image::RgbaImage>>> =
+ 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<Uuid>, 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),