wip: titlebar
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m0s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m36s
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 2m0s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m36s
This commit is contained in:
16
Cargo.lock
generated
16
Cargo.lock
generated
@@ -1317,6 +1317,7 @@ dependencies = [
|
|||||||
"smol",
|
"smol",
|
||||||
"state",
|
"state",
|
||||||
"theme",
|
"theme",
|
||||||
|
"titlebar",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"ui",
|
"ui",
|
||||||
]
|
]
|
||||||
@@ -6570,6 +6571,21 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "titlebar"
|
||||||
|
version = "0.3.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"common",
|
||||||
|
"gpui",
|
||||||
|
"linicon",
|
||||||
|
"log",
|
||||||
|
"smallvec",
|
||||||
|
"theme",
|
||||||
|
"ui",
|
||||||
|
"windows 0.61.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.49.0"
|
version = "1.49.0"
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ icons = [
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
assets = { path = "../assets" }
|
assets = { path = "../assets" }
|
||||||
ui = { path = "../ui" }
|
ui = { path = "../ui" }
|
||||||
|
titlebar = { path = "../titlebar" }
|
||||||
dock = { path = "../dock" }
|
dock = { path = "../dock" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use gpui::{
|
|||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
use theme::{ActiveTheme, Theme, ThemeRegistry};
|
use theme::{ActiveTheme, Theme, ThemeRegistry};
|
||||||
|
use titlebar::TitleBar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::modal::ModalButtonProps;
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
|
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
|
||||||
@@ -28,6 +29,9 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
|
/// App's Title Bar
|
||||||
|
titlebar: Entity<TitleBar>,
|
||||||
|
|
||||||
/// App's Dock Area
|
/// App's Dock Area
|
||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
|
|
||||||
@@ -38,6 +42,7 @@ pub struct Workspace {
|
|||||||
impl Workspace {
|
impl Workspace {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
|
let titlebar = cx.new(|_| TitleBar::new());
|
||||||
let dock =
|
let dock =
|
||||||
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
|
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
|
||||||
|
|
||||||
@@ -100,6 +105,7 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
titlebar,
|
||||||
dock,
|
dock,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
@@ -278,8 +284,14 @@ impl Render for Workspace {
|
|||||||
.on_action(cx.listener(Self::on_keyring))
|
.on_action(cx.listener(Self::on_keyring))
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
// Dock
|
.child(
|
||||||
.child(self.dock.clone())
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
// Title Bar
|
||||||
|
.child(self.titlebar.clone())
|
||||||
|
// Dock
|
||||||
|
.child(self.dock.clone()),
|
||||||
|
)
|
||||||
// Notifications
|
// Notifications
|
||||||
.children(notification_layer)
|
.children(notification_layer)
|
||||||
// Modals
|
// Modals
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ pub struct ThemeColors {
|
|||||||
pub drop_target_background: Hsla,
|
pub drop_target_background: Hsla,
|
||||||
pub cursor: Hsla,
|
pub cursor: Hsla,
|
||||||
pub selection: Hsla,
|
pub selection: Hsla,
|
||||||
|
|
||||||
|
// System
|
||||||
|
pub titlebar: Hsla,
|
||||||
|
pub titlebar_inactive: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default colors for the theme.
|
/// The default colors for the theme.
|
||||||
@@ -171,6 +175,9 @@ impl ThemeColors {
|
|||||||
drop_target_background: brand().dark_alpha().step_2(),
|
drop_target_background: brand().dark_alpha().step_2(),
|
||||||
cursor: hsl(200., 100., 50.),
|
cursor: hsl(200., 100., 50.),
|
||||||
selection: hsl(200., 100., 50.).alpha(0.25),
|
selection: hsl(200., 100., 50.).alpha(0.25),
|
||||||
|
|
||||||
|
titlebar: neutral().dark().step_2(),
|
||||||
|
titlebar_inactive: neutral().dark().step_3(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
crates/titlebar/Cargo.toml
Normal file
21
crates/titlebar/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "titlebar"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
theme = { path = "../theme" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
|
||||||
|
gpui.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
windows = { version = "0.61", features = ["Wdk_System_SystemServices"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
linicon = "2.3.0"
|
||||||
181
crates/titlebar/src/lib.rs
Normal file
181
crates/titlebar/src/lib.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use gpui::MouseButton;
|
||||||
|
use gpui::{
|
||||||
|
div, px, AnyElement, Context, Decorations, Hsla, InteractiveElement as _, IntoElement,
|
||||||
|
ParentElement, Pixels, Render, StatefulInteractiveElement as _, Styled, Window,
|
||||||
|
WindowControlArea,
|
||||||
|
};
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use theme::{ActiveTheme, PlatformKind, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||||
|
use ui::h_flex;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
use crate::platforms::linux::LinuxWindowControls;
|
||||||
|
use crate::platforms::windows::WindowsWindowControls;
|
||||||
|
|
||||||
|
mod platforms;
|
||||||
|
|
||||||
|
pub struct TitleBar {
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
should_move: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TitleBar {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TitleBar {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
children: smallvec![],
|
||||||
|
should_move: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn height(window: &mut Window) -> Pixels {
|
||||||
|
(1.75 * window.rem_size()).max(px(34.))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn height(_window: &mut Window) -> Pixels {
|
||||||
|
px(32.)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context<Self>) -> Hsla {
|
||||||
|
if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||||
|
if window.is_window_active() && !self.should_move {
|
||||||
|
cx.theme().titlebar
|
||||||
|
} else {
|
||||||
|
cx.theme().titlebar_inactive
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cx.theme().titlebar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_children<T>(&mut self, children: T)
|
||||||
|
where
|
||||||
|
T: IntoIterator<Item = AnyElement>,
|
||||||
|
{
|
||||||
|
self.children = children.into_iter().collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for TitleBar {
|
||||||
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
|
self.children.extend(elements)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for TitleBar {
|
||||||
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let supported_controls = window.window_controls();
|
||||||
|
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()
|
||||||
|
.window_control_area(WindowControlArea::Drag)
|
||||||
|
.h(height)
|
||||||
|
.w_full()
|
||||||
|
.map(|this| {
|
||||||
|
if window.is_fullscreen() {
|
||||||
|
this.px_2()
|
||||||
|
} else if cx.theme().platform.is_mac() {
|
||||||
|
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
||||||
|
.pr_2()
|
||||||
|
.when(children.len() <= 1, |this| {
|
||||||
|
this.pr(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.px_2()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|this| match decorations {
|
||||||
|
Decorations::Server => this,
|
||||||
|
Decorations::Client { tiling } => this
|
||||||
|
.when(!(tiling.top || tiling.right), |el| {
|
||||||
|
el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
})
|
||||||
|
.when(!(tiling.top || tiling.left), |el| {
|
||||||
|
el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.bg(color)
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.content_stretch()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("title-bar")
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.w_full()
|
||||||
|
.when(cx.theme().platform.is_mac(), |this| {
|
||||||
|
this.on_click(|event, window, _| {
|
||||||
|
if event.click_count() == 2 {
|
||||||
|
window.titlebar_double_click();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.when(cx.theme().platform.is_linux(), |this| {
|
||||||
|
this.on_click(|event, window, _| {
|
||||||
|
if event.click_count() == 2 {
|
||||||
|
window.zoom_window();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.children(children),
|
||||||
|
)
|
||||||
|
.when(!window.is_fullscreen(), |this| match cx.theme().platform {
|
||||||
|
PlatformKind::Linux => {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if matches!(decorations, Decorations::Client { .. }) {
|
||||||
|
this.child(LinuxWindowControls::new(None))
|
||||||
|
.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"))]
|
||||||
|
this
|
||||||
|
}
|
||||||
|
PlatformKind::Windows => this.child(WindowsWindowControls::new(height)),
|
||||||
|
PlatformKind::Mac => this,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
221
crates/titlebar/src/platforms/linux.rs
Normal file
221
crates/titlebar/src/platforms/linux.rs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
svg, Action, App, InteractiveElement, IntoElement, MouseButton, ParentElement, RenderOnce,
|
||||||
|
SharedString, StatefulInteractiveElement, Styled, Window,
|
||||||
|
};
|
||||||
|
use linicon::{lookup_icon, IconType};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::{h_flex, Icon, IconName, Sizable};
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct LinuxWindowControls {
|
||||||
|
close_window_action: Option<Box<dyn Action>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinuxWindowControls {
|
||||||
|
pub fn new(close_window_action: Option<Box<dyn Action>>) -> Self {
|
||||||
|
Self {
|
||||||
|
close_window_action,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
.when_some(self.close_window_action, |this, close_action| {
|
||||||
|
this.close_action(close_action)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct WindowControl {
|
||||||
|
kind: LinuxControl,
|
||||||
|
fallback: IconName,
|
||||||
|
close_action: Option<Box<dyn Action>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowControl {
|
||||||
|
pub fn new(kind: LinuxControl, fallback: IconName) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
fallback,
|
||||||
|
close_action: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_action(mut self, action: Box<dyn Action>) -> Self {
|
||||||
|
self.close_action = Some(action);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
.when(is_gnome, |this| {
|
||||||
|
this.bg(cx.theme().ghost_element_background_alt)
|
||||||
|
.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(SharedString::from(path))
|
||||||
|
.size_4()
|
||||||
|
.text_color(cx.theme().text),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.child(Icon::new(self.fallback).small().text_color(cx.theme().text))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_mouse_move(|_, _window, cx| cx.stop_propagation())
|
||||||
|
.on_click(move |_, 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<DesktopEnvironment> = OnceLock::new();
|
||||||
|
static LINUX_CONTROLS: OnceLock<HashMap<LinuxControl, Option<String>>> = 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<LinuxControl, Option<String>> {
|
||||||
|
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.to_string_lossy().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
icons
|
||||||
|
})
|
||||||
|
}
|
||||||
6
crates/titlebar/src/platforms/mac.rs
Normal file
6
crates/titlebar/src/platforms/mac.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/// Use pixels here instead of a rem-based size because the macOS traffic
|
||||||
|
/// lights are a static size, and don't scale with the rest of the UI.
|
||||||
|
///
|
||||||
|
/// Magic number: There is one extra pixel of padding on the left side due to
|
||||||
|
/// the 1px border around the window on macOS apps.
|
||||||
|
pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
|
||||||
4
crates/titlebar/src/platforms/mod.rs
Normal file
4
crates/titlebar/src/platforms/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub mod linux;
|
||||||
|
pub mod mac;
|
||||||
|
pub mod windows;
|
||||||
147
crates/titlebar/src/platforms/windows.rs
Normal file
147
crates/titlebar/src/platforms/windows.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, px, App, ElementId, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels,
|
||||||
|
RenderOnce, Rgba, StatefulInteractiveElement, Styled, Window, WindowControlArea,
|
||||||
|
};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::h_flex;
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct WindowsWindowControls {
|
||||||
|
button_height: Pixels,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowsWindowControls {
|
||||||
|
pub fn new(button_height: Pixels) -> Self {
|
||||||
|
Self { button_height }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn get_font() -> &'static str {
|
||||||
|
"Segoe Fluent Icons"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_font() -> &'static str {
|
||||||
|
use windows::Wdk::System::SystemServices::RtlGetVersion;
|
||||||
|
|
||||||
|
let mut version = unsafe { std::mem::zeroed() };
|
||||||
|
let status = unsafe { RtlGetVersion(&mut version) };
|
||||||
|
|
||||||
|
if status.is_ok() && version.dwBuildNumber >= 22000 {
|
||||||
|
"Segoe Fluent Icons"
|
||||||
|
} else {
|
||||||
|
"Segoe MDL2 Assets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for WindowsWindowControls {
|
||||||
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
|
let close_button_hover_color = Rgba {
|
||||||
|
r: 232.0 / 255.0,
|
||||||
|
g: 17.0 / 255.0,
|
||||||
|
b: 32.0 / 255.0,
|
||||||
|
a: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let button_hover_color = cx.theme().ghost_element_hover;
|
||||||
|
let button_active_color = cx.theme().ghost_element_active;
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id("windows-window-controls")
|
||||||
|
.font_family(Self::get_font())
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.justify_center()
|
||||||
|
.content_stretch()
|
||||||
|
.max_h(self.button_height)
|
||||||
|
.min_h(self.button_height)
|
||||||
|
.child(WindowsCaptionButton::new(
|
||||||
|
"minimize",
|
||||||
|
WindowsCaptionButtonIcon::Minimize,
|
||||||
|
button_hover_color,
|
||||||
|
button_active_color,
|
||||||
|
))
|
||||||
|
.child(WindowsCaptionButton::new(
|
||||||
|
"maximize-or-restore",
|
||||||
|
if window.is_maximized() {
|
||||||
|
WindowsCaptionButtonIcon::Restore
|
||||||
|
} else {
|
||||||
|
WindowsCaptionButtonIcon::Maximize
|
||||||
|
},
|
||||||
|
button_hover_color,
|
||||||
|
button_active_color,
|
||||||
|
))
|
||||||
|
.child(WindowsCaptionButton::new(
|
||||||
|
"close",
|
||||||
|
WindowsCaptionButtonIcon::Close,
|
||||||
|
close_button_hover_color,
|
||||||
|
button_active_color,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
enum WindowsCaptionButtonIcon {
|
||||||
|
Minimize,
|
||||||
|
Restore,
|
||||||
|
Maximize,
|
||||||
|
Close,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
struct WindowsCaptionButton {
|
||||||
|
id: ElementId,
|
||||||
|
icon: WindowsCaptionButtonIcon,
|
||||||
|
hover_background_color: Hsla,
|
||||||
|
active_background_color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowsCaptionButton {
|
||||||
|
pub fn new(
|
||||||
|
id: impl Into<ElementId>,
|
||||||
|
icon: WindowsCaptionButtonIcon,
|
||||||
|
hover_background_color: impl Into<Hsla>,
|
||||||
|
active_background_color: impl Into<Hsla>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
icon,
|
||||||
|
hover_background_color: hover_background_color.into(),
|
||||||
|
active_background_color: active_background_color.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for WindowsCaptionButton {
|
||||||
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.id(self.id)
|
||||||
|
.justify_center()
|
||||||
|
.content_center()
|
||||||
|
.occlude()
|
||||||
|
.w(px(36.))
|
||||||
|
.h_full()
|
||||||
|
.text_size(px(10.0))
|
||||||
|
.hover(|style| style.bg(self.hover_background_color))
|
||||||
|
.active(|style| style.bg(self.active_background_color))
|
||||||
|
.map(|this| match self.icon {
|
||||||
|
WindowsCaptionButtonIcon::Close => {
|
||||||
|
this.window_control_area(WindowControlArea::Close)
|
||||||
|
}
|
||||||
|
WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => {
|
||||||
|
this.window_control_area(WindowControlArea::Max)
|
||||||
|
}
|
||||||
|
WindowsCaptionButtonIcon::Minimize => {
|
||||||
|
this.window_control_area(WindowControlArea::Min)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(match self.icon {
|
||||||
|
WindowsCaptionButtonIcon::Minimize => "\u{e921}",
|
||||||
|
WindowsCaptionButtonIcon::Restore => "\u{e923}",
|
||||||
|
WindowsCaptionButtonIcon::Maximize => "\u{e922}",
|
||||||
|
WindowsCaptionButtonIcon::Close => "\u{e8bb}",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user