use std::collections::HashMap; use std::path::PathBuf; use std::sync::OnceLock; use gpui::prelude::FluentBuilder; use gpui::{ svg, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce, StatefulInteractiveElement, Styled, Window, }; use linicon::{lookup_icon, IconType}; use theme::ActiveTheme; use ui::{h_flex, Icon, IconName, Sizable}; #[derive(IntoElement)] pub struct LinuxWindowControls {} impl LinuxWindowControls { pub fn new() -> Self { Self {} } } impl RenderOnce for LinuxWindowControls { fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { 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) } }) .child(WindowControl::new( LinuxControl::Close, IconName::WindowClose, )) } } #[derive(IntoElement)] pub struct WindowControl { kind: LinuxControl, fallback: IconName, } impl WindowControl { pub fn new(kind: LinuxControl, fallback: IconName) -> Self { Self { kind, fallback } } pub fn is_gnome(&self) -> bool { matches!(detect_desktop_environment(), DesktopEnvironment::Gnome) } } impl RenderOnce for WindowControl { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let is_gnome = self.is_gnome(); h_flex() .id(self.kind.as_icon_name()) .group("") .justify_center() .items_center() .rounded_full() .size_6() .map(|this| { if is_gnome { this.bg(cx.theme().tab_inactive_background) .hover(|this| this.bg(cx.theme().tab_hover_background)) .active(|this| this.bg(cx.theme().tab_active_background)) } else { this.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| { if let Some(Some(path)) = linux_controls().get(&self.kind).cloned() { this.child( svg() .external_path(path.into_os_string().into_string().unwrap()) .map(|this| { if cx.theme().is_dark() { this.text_color(gpui::white()) } else { this.text_color(gpui::black()) } }) .size_4(), ) } else { this.child(Icon::new(self.fallback).flex_grow().small()) } }) .on_mouse_move(|_ev, _window, cx| cx.stop_propagation()) .on_click(move |_ev, window, cx| { cx.stop_propagation(); match self.kind { LinuxControl::Minimize => window.minimize_window(), LinuxControl::Restore => window.zoom_window(), LinuxControl::Maximize => window.zoom_window(), LinuxControl::Close => cx.quit(), } }) } } static DE: OnceLock = OnceLock::new(); static LINUX_CONTROLS: OnceLock>> = OnceLock::new(); #[derive(Debug, Clone, Copy, PartialEq)] pub enum DesktopEnvironment { Gnome, Kde, Unknown, } /// Detect the current desktop environment pub fn detect_desktop_environment() -> &'static DesktopEnvironment { DE.get_or_init(|| { // Try to use environment variables first if let Ok(output) = std::env::var("XDG_CURRENT_DESKTOP") { let desktop = output.to_lowercase(); if desktop.contains("gnome") { return DesktopEnvironment::Gnome; } else if desktop.contains("kde") { return DesktopEnvironment::Kde; } } // Fallback detection methods if let Ok(output) = std::env::var("DESKTOP_SESSION") { let session = output.to_lowercase(); if session.contains("gnome") { return DesktopEnvironment::Gnome; } else if session.contains("kde") || session.contains("plasma") { return DesktopEnvironment::Kde; } } DesktopEnvironment::Unknown }) } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub enum LinuxControl { Minimize, Restore, Maximize, Close, } impl LinuxControl { pub fn as_icon_name(&self) -> &'static str { match self { LinuxControl::Close => "window-close", LinuxControl::Minimize => "window-minimize", LinuxControl::Maximize => "window-maximize", LinuxControl::Restore => "window-restore", } } } fn linux_controls() -> &'static HashMap> { LINUX_CONTROLS.get_or_init(|| { let mut icons = HashMap::new(); icons.insert(LinuxControl::Close, None); icons.insert(LinuxControl::Minimize, None); icons.insert(LinuxControl::Maximize, None); icons.insert(LinuxControl::Restore, None); let icon_names = [ (LinuxControl::Close, vec!["window-close", "dialog-close"]), ( LinuxControl::Minimize, vec!["window-minimize", "window-lower"], ), ( LinuxControl::Maximize, vec!["window-maximize", "window-expand"], ), ( LinuxControl::Restore, vec!["window-restore", "window-return"], ), ]; for (control, icon_names) in icon_names { for icon_name in icon_names { // Try GNOME-style naming first let mut control_icon = lookup_icon(format!("{icon_name}-symbolic")) .find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG)); // If not found, try KDE-style naming if control_icon.is_none() { control_icon = lookup_icon(icon_name) .find(|icon| matches!(icon, Ok(icon) if icon.icon_type == IconType::SVG)); } if let Some(Ok(icon)) = control_icon { icons.entry(control).and_modify(|v| *v = Some(icon.path)); } } } icons }) }