**TODOs:** - [x] Fix all clippy issues - [x] Make NIP-4e optional (disabled by default) - [x] Remove support for bunker (Nostr Connect) - [x] Group messages in the same timeframe - [ ] ... Reviewed-on: #34
353 lines
11 KiB
Rust
353 lines
11 KiB
Rust
use std::rc::Rc;
|
|
|
|
use gpui::prelude::FluentBuilder as _;
|
|
use gpui::{
|
|
AnyElement, App, ClickEvent, Context, Decorations, Hsla, InteractiveElement, IntoElement,
|
|
MouseButton, ParentElement, Pixels, Render, RenderOnce, StatefulInteractiveElement as _,
|
|
StyleRefinement, Styled, TitlebarOptions, Window, WindowControlArea, div, px,
|
|
};
|
|
use smallvec::SmallVec;
|
|
use theme::ActiveTheme;
|
|
|
|
use crate::{Icon, IconName, InteractiveElementExt as _, Sizable as _, StyledExt, h_flex};
|
|
|
|
pub const TITLE_BAR_HEIGHT: Pixels = px(34.);
|
|
#[cfg(target_os = "macos")]
|
|
pub const TRAFFIC_LIGHT_PADDING: f32 = 80.;
|
|
|
|
/// TitleBar used to customize the appearance of the title bar.
|
|
///
|
|
/// We can put some elements inside the title bar.
|
|
#[derive(IntoElement)]
|
|
#[allow(clippy::type_complexity)]
|
|
pub struct TitleBar {
|
|
style: StyleRefinement,
|
|
children: SmallVec<[AnyElement; 1]>,
|
|
on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
|
|
}
|
|
|
|
impl TitleBar {
|
|
/// Create a new TitleBar.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
style: StyleRefinement::default(),
|
|
children: SmallVec::new(),
|
|
on_close_window: None,
|
|
}
|
|
}
|
|
|
|
/// Returns the default title bar options for compatible with the [`crate::TitleBar`].
|
|
pub fn title_bar_options() -> TitlebarOptions {
|
|
TitlebarOptions {
|
|
title: None,
|
|
appears_transparent: true,
|
|
traffic_light_position: Some(gpui::point(px(9.0), px(9.0))),
|
|
}
|
|
}
|
|
|
|
/// Add custom for close window event, default is None, then click X button will call `window.remove_window()`.
|
|
/// Linux only, this will do nothing on other platforms.
|
|
pub fn on_close_window(
|
|
mut self,
|
|
f: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
|
) -> Self {
|
|
if cfg!(target_os = "linux") {
|
|
self.on_close_window = Some(Rc::new(Box::new(f)));
|
|
}
|
|
self
|
|
}
|
|
}
|
|
|
|
impl Default for TitleBar {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
// The Windows control buttons have a fixed width of 35px.
|
|
//
|
|
// We don't need implementation the click event for the control buttons.
|
|
// If user clicked in the bounds, the window event will be triggered.
|
|
#[derive(IntoElement, Clone)]
|
|
#[allow(clippy::type_complexity)]
|
|
enum ControlIcon {
|
|
Minimize,
|
|
Restore,
|
|
Maximize,
|
|
Close {
|
|
on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
|
|
},
|
|
}
|
|
|
|
impl ControlIcon {
|
|
fn minimize() -> Self {
|
|
Self::Minimize
|
|
}
|
|
|
|
fn restore() -> Self {
|
|
Self::Restore
|
|
}
|
|
|
|
fn maximize() -> Self {
|
|
Self::Maximize
|
|
}
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
fn close(on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>) -> Self {
|
|
Self::Close { on_close_window }
|
|
}
|
|
|
|
fn id(&self) -> &'static str {
|
|
match self {
|
|
Self::Minimize => "minimize",
|
|
Self::Restore => "restore",
|
|
Self::Maximize => "maximize",
|
|
Self::Close { .. } => "close",
|
|
}
|
|
}
|
|
|
|
fn icon(&self) -> IconName {
|
|
match self {
|
|
Self::Minimize => IconName::WindowMinimize,
|
|
Self::Restore => IconName::WindowRestore,
|
|
Self::Maximize => IconName::WindowMaximize,
|
|
Self::Close { .. } => IconName::WindowClose,
|
|
}
|
|
}
|
|
|
|
fn window_control_area(&self) -> WindowControlArea {
|
|
match self {
|
|
Self::Minimize => WindowControlArea::Min,
|
|
Self::Restore | Self::Maximize => WindowControlArea::Max,
|
|
Self::Close { .. } => WindowControlArea::Close,
|
|
}
|
|
}
|
|
|
|
fn is_close(&self) -> bool {
|
|
matches!(self, Self::Close { .. })
|
|
}
|
|
|
|
#[inline]
|
|
fn hover_fg(&self, cx: &App) -> Hsla {
|
|
if self.is_close() {
|
|
cx.theme().danger_foreground
|
|
} else {
|
|
cx.theme().text
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn hover_bg(&self, cx: &App) -> Hsla {
|
|
if self.is_close() {
|
|
cx.theme().danger_background
|
|
} else {
|
|
cx.theme().ghost_element_hover
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn active_bg(&self, cx: &mut App) -> Hsla {
|
|
if self.is_close() {
|
|
cx.theme().danger_active
|
|
} else {
|
|
cx.theme().ghost_element_active
|
|
}
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for ControlIcon {
|
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let is_linux = cfg!(target_os = "linux");
|
|
let is_windows = cfg!(target_os = "windows");
|
|
|
|
let icon = self.clone();
|
|
let hover_fg = self.hover_fg(cx);
|
|
let hover_bg = self.hover_bg(cx);
|
|
let active_bg = self.active_bg(cx);
|
|
|
|
let on_close_window = match &self {
|
|
ControlIcon::Close { on_close_window } => on_close_window.clone(),
|
|
_ => None,
|
|
};
|
|
|
|
div()
|
|
.id(self.id())
|
|
.flex()
|
|
.w(TITLE_BAR_HEIGHT)
|
|
.h_full()
|
|
.flex_shrink_0()
|
|
.justify_center()
|
|
.content_center()
|
|
.items_center()
|
|
.text_color(cx.theme().text)
|
|
.hover(|style| style.bg(hover_bg).text_color(hover_fg))
|
|
.active(|style| style.bg(active_bg).text_color(hover_fg))
|
|
.when(is_windows, |this| {
|
|
this.window_control_area(self.window_control_area())
|
|
})
|
|
.when(is_linux, |this| {
|
|
this.on_mouse_down(MouseButton::Left, move |_, window, cx| {
|
|
window.prevent_default();
|
|
cx.stop_propagation();
|
|
})
|
|
.on_click(move |_, window, cx| {
|
|
cx.stop_propagation();
|
|
match icon {
|
|
Self::Minimize => window.minimize_window(),
|
|
Self::Restore | Self::Maximize => window.zoom_window(),
|
|
Self::Close { .. } => {
|
|
if let Some(f) = on_close_window.clone() {
|
|
f(&ClickEvent::default(), window, cx);
|
|
} else {
|
|
window.remove_window();
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
.child(Icon::new(self.icon()).small())
|
|
}
|
|
}
|
|
|
|
#[derive(IntoElement)]
|
|
#[allow(clippy::type_complexity)]
|
|
struct WindowControls {
|
|
on_close_window: Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>,
|
|
}
|
|
|
|
impl RenderOnce for WindowControls {
|
|
fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
|
|
if cfg!(target_os = "macos") || cfg!(target_family = "wasm") {
|
|
return div().id("window-controls");
|
|
}
|
|
|
|
h_flex()
|
|
.id("window-controls")
|
|
.items_center()
|
|
.flex_shrink_0()
|
|
.h_full()
|
|
.child(ControlIcon::minimize())
|
|
.child(if window.is_maximized() {
|
|
ControlIcon::restore()
|
|
} else {
|
|
ControlIcon::maximize()
|
|
})
|
|
.child(ControlIcon::close(self.on_close_window))
|
|
}
|
|
}
|
|
|
|
impl Styled for TitleBar {
|
|
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
|
&mut self.style
|
|
}
|
|
}
|
|
|
|
impl ParentElement for TitleBar {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.children.extend(elements);
|
|
}
|
|
}
|
|
|
|
struct TitleBarState {
|
|
should_move: bool,
|
|
}
|
|
|
|
impl Render for TitleBarState {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
div()
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for TitleBar {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let is_client_decorated = matches!(window.window_decorations(), Decorations::Client { .. });
|
|
let is_web = cfg!(target_family = "wasm");
|
|
let is_linux = cfg!(target_os = "linux");
|
|
let is_macos = cfg!(target_os = "macos");
|
|
|
|
let state = window.use_state(cx, |_, _| TitleBarState { should_move: false });
|
|
|
|
div().flex_shrink_0().child(
|
|
div()
|
|
.id("title-bar")
|
|
.flex()
|
|
.flex_row()
|
|
.items_center()
|
|
.justify_between()
|
|
.h(TITLE_BAR_HEIGHT)
|
|
.map(|this| {
|
|
if window.is_fullscreen() {
|
|
this.px_2()
|
|
} else if cx.theme().platform.is_mac() {
|
|
this.pr_2().pl(px(TRAFFIC_LIGHT_PADDING))
|
|
} else {
|
|
this.px_2()
|
|
}
|
|
})
|
|
.border_b_1()
|
|
.border_color(cx.theme().border)
|
|
.bg(cx.theme().title_bar)
|
|
.refine_style(&self.style)
|
|
.when(is_linux, |this| {
|
|
this.on_double_click(|_, window, _| window.zoom_window())
|
|
})
|
|
.when(is_macos, |this| {
|
|
this.on_double_click(|_, window, _| window.titlebar_double_click())
|
|
})
|
|
.on_mouse_down_out(window.listener_for(&state, |state, _, _, _| {
|
|
state.should_move = false;
|
|
}))
|
|
.on_mouse_down(
|
|
MouseButton::Left,
|
|
window.listener_for(&state, |state, _, _, _| {
|
|
state.should_move = true;
|
|
}),
|
|
)
|
|
.on_mouse_up(
|
|
MouseButton::Left,
|
|
window.listener_for(&state, |state, _, _, _| {
|
|
state.should_move = false;
|
|
}),
|
|
)
|
|
.on_mouse_move(window.listener_for(&state, |state, _, window, _| {
|
|
if state.should_move {
|
|
state.should_move = false;
|
|
window.start_window_move();
|
|
}
|
|
}))
|
|
.child(
|
|
h_flex()
|
|
.id("bar")
|
|
.h_full()
|
|
.justify_between()
|
|
.flex_shrink_0()
|
|
.flex_1()
|
|
.when(!is_web, |this| {
|
|
this.window_control_area(WindowControlArea::Drag)
|
|
.when(window.is_fullscreen(), |this| this.pl_3())
|
|
.when(is_linux && is_client_decorated, |this| {
|
|
this.child(
|
|
div()
|
|
.top_0()
|
|
.left_0()
|
|
.absolute()
|
|
.size_full()
|
|
.h_full()
|
|
.on_mouse_down(
|
|
MouseButton::Right,
|
|
move |ev, window, _| {
|
|
window.show_window_menu(ev.position)
|
|
},
|
|
),
|
|
)
|
|
})
|
|
})
|
|
.children(self.children),
|
|
)
|
|
.child(WindowControls {
|
|
on_close_window: self.on_close_window,
|
|
}),
|
|
)
|
|
}
|
|
}
|