chore: Refine the UI (#102)
* update deps * update window options * linux title bar * fix build * . * fix build * rounded corners on linux * . * . * fix i18n key * fix change subject modal * . * update new account * . * update relay modal * . * fix i18n keys --------- Co-authored-by: reya <reya@macbook.local>
This commit is contained in:
25
crates/title_bar/Cargo.toml
Normal file
25
crates/title_bar/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "title_bar"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
theme = { path = "../theme" }
|
||||
ui = { path = "../ui" }
|
||||
|
||||
rust-i18n.workspace = true
|
||||
i18n.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
gpui.workspace = true
|
||||
smol.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"
|
||||
178
crates/title_bar/src/lib.rs
Normal file
178
crates/title_bar/src/lib.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
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::platform_kind::PlatformKind;
|
||||
use theme::{ActiveTheme, 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().title_bar
|
||||
} else {
|
||||
cx.theme().title_bar_inactive
|
||||
}
|
||||
} else {
|
||||
cx.theme().title_bar
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.w_full()
|
||||
.h(height)
|
||||
.map(|this| {
|
||||
if window.is_fullscreen() {
|
||||
this.pl_2()
|
||||
} else if cx.theme().platform_kind.is_mac() {
|
||||
this.pl(px(platforms::mac::TRAFFIC_LIGHT_PADDING))
|
||||
} else {
|
||||
this.pl_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)
|
||||
.content_stretch()
|
||||
.child(
|
||||
div()
|
||||
.id("title-bar")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.when(cx.theme().platform_kind.is_mac(), |this| {
|
||||
this.on_click(|event, window, _| {
|
||||
if event.up.click_count == 2 {
|
||||
window.titlebar_double_click();
|
||||
}
|
||||
})
|
||||
})
|
||||
.when(cx.theme().platform_kind.is_linux(), |this| {
|
||||
this.on_click(|event, window, _| {
|
||||
if event.up.click_count == 2 {
|
||||
window.zoom_window();
|
||||
}
|
||||
})
|
||||
})
|
||||
.children(children),
|
||||
)
|
||||
.when(!window.is_fullscreen(), |this| {
|
||||
match cx.theme().platform_kind {
|
||||
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,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
229
crates/title_bar/src/platforms/linux.rs
Normal file
229
crates/title_bar/src/platforms/linux.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
img, Action, 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 {
|
||||
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")
|
||||
.px_2()
|
||||
.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()
|
||||
.map(|this| {
|
||||
if is_gnome {
|
||||
this.size_6()
|
||||
.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.size_5()
|
||||
.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(img(path).flex_grow().size_4())
|
||||
} else {
|
||||
this.child(Icon::new(self.fallback).flex_grow().small())
|
||||
}
|
||||
})
|
||||
.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 => window.dispatch_action(
|
||||
self.close_action
|
||||
.as_ref()
|
||||
.expect("Use WindowControl::new_close() for close control.")
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static DE: OnceLock<DesktopEnvironment> = OnceLock::new();
|
||||
static LINUX_CONTROLS: OnceLock<HashMap<LinuxControl, Option<PathBuf>>> = 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<PathBuf>> {
|
||||
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
|
||||
})
|
||||
}
|
||||
6
crates/title_bar/src/platforms/mac.rs
Normal file
6
crates/title_bar/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 = 71.;
|
||||
4
crates/title_bar/src/platforms/mod.rs
Normal file
4
crates/title_bar/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/title_bar/src/platforms/windows.rs
Normal file
147
crates/title_bar/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