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:
@@ -8,7 +8,7 @@ use theme::ActiveTheme;
|
||||
|
||||
use crate::indicator::Indicator;
|
||||
use crate::tooltip::Tooltip;
|
||||
use crate::{Disableable, Icon, Selectable, Sizable, Size, StyledExt};
|
||||
use crate::{h_flex, Disableable, Icon, Selectable, Sizable, Size, StyledExt};
|
||||
|
||||
pub enum ButtonRounded {
|
||||
Normal,
|
||||
@@ -48,7 +48,12 @@ pub trait ButtonVariants: Sized {
|
||||
|
||||
/// With the ghost style for the Button.
|
||||
fn ghost(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Ghost)
|
||||
self.with_variant(ButtonVariant::Ghost { alt: false })
|
||||
}
|
||||
|
||||
/// With the ghost style for the Button.
|
||||
fn ghost_alt(self) -> Self {
|
||||
self.with_variant(ButtonVariant::Ghost { alt: true })
|
||||
}
|
||||
|
||||
/// With the transparent style for the Button.
|
||||
@@ -100,7 +105,7 @@ pub enum ButtonVariant {
|
||||
Secondary,
|
||||
Danger,
|
||||
Warning,
|
||||
Ghost,
|
||||
Ghost { alt: bool },
|
||||
Transparent,
|
||||
Custom(ButtonCustomVariant),
|
||||
}
|
||||
@@ -118,19 +123,26 @@ type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>
|
||||
pub struct Button {
|
||||
pub base: Div,
|
||||
id: ElementId,
|
||||
|
||||
icon: Option<Icon>,
|
||||
label: Option<SharedString>,
|
||||
tooltip: Option<SharedString>,
|
||||
children: Vec<AnyElement>,
|
||||
disabled: bool,
|
||||
|
||||
variant: ButtonVariant,
|
||||
rounded: ButtonRounded,
|
||||
size: Size,
|
||||
|
||||
disabled: bool,
|
||||
reverse: bool,
|
||||
bold: bool,
|
||||
tooltip: Option<SharedString>,
|
||||
on_click: OnClick,
|
||||
cta: bool,
|
||||
|
||||
loading: bool,
|
||||
loading_icon: Option<Icon>,
|
||||
|
||||
on_click: OnClick,
|
||||
|
||||
pub(crate) selected: bool,
|
||||
pub(crate) stop_propagation: bool,
|
||||
}
|
||||
@@ -159,6 +171,7 @@ impl Button {
|
||||
loading: false,
|
||||
reverse: false,
|
||||
bold: false,
|
||||
cta: false,
|
||||
children: Vec::new(),
|
||||
loading_icon: None,
|
||||
}
|
||||
@@ -200,28 +213,38 @@ impl Button {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set bold the button (label will be use the semi-bold font).
|
||||
pub fn bold(mut self) -> Self {
|
||||
self.bold = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
/// Set the cta style of the button.
|
||||
pub fn cta(mut self) -> Self {
|
||||
self.cta = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the stop propagation of the button.
|
||||
pub fn stop_propagation(mut self, val: bool) -> Self {
|
||||
self.stop_propagation = val;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the loading icon of the button.
|
||||
pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.loading_icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the click handler of the button.
|
||||
pub fn on_click<C>(mut self, handler: C) -> Self
|
||||
where
|
||||
C: Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for Button {
|
||||
@@ -280,7 +303,7 @@ impl RenderOnce for Button {
|
||||
let normal_style = style.normal(window, cx);
|
||||
let icon_size = match self.size {
|
||||
Size::Size(v) => Size::Size(v * 0.75),
|
||||
Size::Medium => Size::Small,
|
||||
Size::Large => Size::Medium,
|
||||
_ => self.size,
|
||||
};
|
||||
|
||||
@@ -300,25 +323,67 @@ impl RenderOnce for Button {
|
||||
// Icon Button
|
||||
match self.size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_5(),
|
||||
Size::Small => this.size_6(),
|
||||
Size::Medium => this.size_7(),
|
||||
_ => this.size_9(),
|
||||
Size::XSmall => {
|
||||
if self.cta {
|
||||
this.w_10().h_5()
|
||||
} else {
|
||||
this.size_5()
|
||||
}
|
||||
}
|
||||
Size::Small => {
|
||||
if self.cta {
|
||||
this.w_12().h_6()
|
||||
} else {
|
||||
this.size_6()
|
||||
}
|
||||
}
|
||||
Size::Medium => {
|
||||
if self.cta {
|
||||
this.w_12().h_7()
|
||||
} else {
|
||||
this.size_7()
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if self.cta {
|
||||
this.w_16().h_9()
|
||||
} else {
|
||||
this.size_9()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal Button
|
||||
match self.size {
|
||||
Size::Size(size) => this.px(size * 0.2),
|
||||
Size::XSmall => this.h_6().px_2(),
|
||||
Size::Small => {
|
||||
Size::XSmall => {
|
||||
if self.icon.is_some() {
|
||||
this.h_7().pl_2().pr_3()
|
||||
this.h_6().pl_2().pr_2p5()
|
||||
} else {
|
||||
this.h_7().px_3()
|
||||
this.h_6().px_2()
|
||||
}
|
||||
}
|
||||
Size::Small => {
|
||||
if self.icon.is_some() {
|
||||
this.h_7().pl_2().pr_2p5()
|
||||
} else {
|
||||
this.h_7().px_2()
|
||||
}
|
||||
}
|
||||
Size::Medium => {
|
||||
if self.icon.is_some() {
|
||||
this.h_8().pl_3().pr_3p5()
|
||||
} else {
|
||||
this.h_8().px_3()
|
||||
}
|
||||
}
|
||||
Size::Large => {
|
||||
if self.icon.is_some() {
|
||||
this.h_10().px_3().pr_3p5()
|
||||
} else {
|
||||
this.h_10().px_3()
|
||||
}
|
||||
}
|
||||
Size::Medium => this.h_8().px_3(),
|
||||
Size::Large => this.h_10().px_4(),
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -346,17 +411,14 @@ impl RenderOnce for Button {
|
||||
.shadow_none()
|
||||
})
|
||||
.child({
|
||||
div()
|
||||
.flex()
|
||||
.when(self.reverse, |this| this.flex_row_reverse())
|
||||
h_flex()
|
||||
.id("label")
|
||||
.items_center()
|
||||
.when(self.reverse, |this| this.flex_row_reverse())
|
||||
.justify_center()
|
||||
.text_sm()
|
||||
.map(|this| match self.size {
|
||||
Size::XSmall => this.gap_0p5(),
|
||||
Size::Small => this.gap_1(),
|
||||
_ => this.gap_2().font_medium(),
|
||||
Size::XSmall => this.text_xs().gap_1(),
|
||||
Size::Small => this.text_sm().gap_1p5(),
|
||||
_ => this.text_sm().gap_2(),
|
||||
})
|
||||
.when(!self.loading, |this| {
|
||||
this.when_some(self.icon, |this, icon| {
|
||||
@@ -421,8 +483,15 @@ impl ButtonVariant {
|
||||
ButtonVariant::Secondary => cx.theme().elevated_surface_background,
|
||||
ButtonVariant::Danger => cx.theme().danger_background,
|
||||
ButtonVariant::Warning => cx.theme().warning_background,
|
||||
ButtonVariant::Ghost { alt } => {
|
||||
if *alt {
|
||||
cx.theme().ghost_element_background_alt
|
||||
} else {
|
||||
cx.theme().ghost_element_background
|
||||
}
|
||||
}
|
||||
ButtonVariant::Custom(colors) => colors.color,
|
||||
_ => cx.theme().ghost_element_background,
|
||||
_ => gpui::transparent_black(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,7 +502,13 @@ impl ButtonVariant {
|
||||
ButtonVariant::Danger => cx.theme().danger_foreground,
|
||||
ButtonVariant::Warning => cx.theme().warning_foreground,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
ButtonVariant::Ghost => cx.theme().text_muted,
|
||||
ButtonVariant::Ghost { alt } => {
|
||||
if *alt {
|
||||
cx.theme().text
|
||||
} else {
|
||||
cx.theme().text_muted
|
||||
}
|
||||
}
|
||||
ButtonVariant::Custom(colors) => colors.foreground,
|
||||
}
|
||||
}
|
||||
@@ -444,14 +519,14 @@ impl ButtonVariant {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_hover,
|
||||
ButtonVariant::Danger => cx.theme().danger_hover,
|
||||
ButtonVariant::Warning => cx.theme().warning_hover,
|
||||
ButtonVariant::Ghost => cx.theme().ghost_element_hover,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_hover,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.hover,
|
||||
};
|
||||
|
||||
let fg = match self {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_foreground,
|
||||
ButtonVariant::Ghost => cx.theme().text,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().text,
|
||||
ButtonVariant::Transparent => cx.theme().text_placeholder,
|
||||
_ => self.text_color(window, cx),
|
||||
};
|
||||
@@ -465,7 +540,7 @@ impl ButtonVariant {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_active,
|
||||
ButtonVariant::Danger => cx.theme().danger_active,
|
||||
ButtonVariant::Warning => cx.theme().warning_active,
|
||||
ButtonVariant::Ghost => cx.theme().ghost_element_active,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_active,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.active,
|
||||
};
|
||||
@@ -485,7 +560,7 @@ impl ButtonVariant {
|
||||
ButtonVariant::Secondary => cx.theme().secondary_selected,
|
||||
ButtonVariant::Danger => cx.theme().danger_selected,
|
||||
ButtonVariant::Warning => cx.theme().warning_selected,
|
||||
ButtonVariant::Ghost => cx.theme().ghost_element_selected,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_selected,
|
||||
ButtonVariant::Transparent => gpui::transparent_black(),
|
||||
ButtonVariant::Custom(colors) => colors.active,
|
||||
};
|
||||
@@ -501,7 +576,9 @@ impl ButtonVariant {
|
||||
|
||||
fn disabled(&self, _window: &Window, cx: &App) -> ButtonVariantStyle {
|
||||
let bg = match self {
|
||||
ButtonVariant::Ghost => cx.theme().ghost_element_disabled,
|
||||
ButtonVariant::Danger => cx.theme().danger_disabled,
|
||||
ButtonVariant::Warning => cx.theme().warning_disabled,
|
||||
ButtonVariant::Ghost { .. } => cx.theme().ghost_element_disabled,
|
||||
ButtonVariant::Secondary => cx.theme().secondary_disabled,
|
||||
_ => cx.theme().element_disabled,
|
||||
};
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, Action, App, AppContext, Corner, Element, InteractiveElement, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity,
|
||||
Window,
|
||||
div, px, App, AppContext, Corner, Element, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, Styled, WeakEntity, Window,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::button::{Button, ButtonVariants};
|
||||
use crate::input::InputState;
|
||||
use crate::popover::{Popover, PopoverContent};
|
||||
use crate::Icon;
|
||||
use crate::{Icon, Sizable, Size};
|
||||
|
||||
/// Emit a emoji to target input
|
||||
#[derive(Action, PartialEq, Clone, Debug, Deserialize)]
|
||||
#[action(namespace = emoji, no_json)]
|
||||
pub struct EmitEmoji(pub SharedString);
|
||||
static EMOJIS: OnceLock<Vec<SharedString>> = OnceLock::new();
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct EmojiPicker {
|
||||
icon: Option<Icon>,
|
||||
anchor: Option<Corner>,
|
||||
target_input: WeakEntity<InputState>,
|
||||
emojis: Rc<Vec<SharedString>>,
|
||||
}
|
||||
|
||||
impl EmojiPicker {
|
||||
pub fn new(target_input: WeakEntity<InputState>) -> Self {
|
||||
fn get_emojis() -> &'static Vec<SharedString> {
|
||||
EMOJIS.get_or_init(|| {
|
||||
let mut emojis: Vec<SharedString> = vec![];
|
||||
|
||||
emojis.extend(
|
||||
@@ -38,9 +25,23 @@ impl EmojiPicker {
|
||||
.collect::<Vec<SharedString>>(),
|
||||
);
|
||||
|
||||
emojis
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct EmojiPicker {
|
||||
icon: Option<Icon>,
|
||||
size: Size,
|
||||
anchor: Option<Corner>,
|
||||
target_input: WeakEntity<InputState>,
|
||||
}
|
||||
|
||||
impl EmojiPicker {
|
||||
pub fn new(target_input: WeakEntity<InputState>) -> Self {
|
||||
Self {
|
||||
target_input,
|
||||
emojis: emojis.into(),
|
||||
size: Size::default(),
|
||||
anchor: None,
|
||||
icon: None,
|
||||
}
|
||||
@@ -57,6 +58,13 @@ impl EmojiPicker {
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for EmojiPicker {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for EmojiPicker {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
Popover::new("emoji-picker")
|
||||
@@ -70,10 +78,10 @@ impl RenderOnce for EmojiPicker {
|
||||
.trigger(
|
||||
Button::new("emoji-trigger")
|
||||
.when_some(self.icon, |this, icon| this.icon(icon))
|
||||
.ghost(),
|
||||
.ghost()
|
||||
.with_size(self.size),
|
||||
)
|
||||
.content(move |window, cx| {
|
||||
let emojis = self.emojis.clone();
|
||||
let input = self.target_input.clone();
|
||||
|
||||
cx.new(|cx| {
|
||||
@@ -83,7 +91,7 @@ impl RenderOnce for EmojiPicker {
|
||||
.flex_wrap()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.children(emojis.iter().map(|e| {
|
||||
.children(get_emojis().iter().map(|e| {
|
||||
div()
|
||||
.id(e.clone())
|
||||
.flex_auto()
|
||||
|
||||
@@ -3,7 +3,6 @@ pub use focusable::FocusableCycle;
|
||||
pub use icon::*;
|
||||
pub use root::{ContextModal, Root};
|
||||
pub use styled::*;
|
||||
pub use title_bar::*;
|
||||
pub use window_border::{window_border, WindowBorder};
|
||||
|
||||
pub use crate::Disableable;
|
||||
@@ -39,7 +38,6 @@ mod focusable;
|
||||
mod icon;
|
||||
mod root;
|
||||
mod styled;
|
||||
mod title_bar;
|
||||
mod window_border;
|
||||
|
||||
i18n::init!();
|
||||
|
||||
@@ -45,7 +45,7 @@ impl Default for ModalButtonProps {
|
||||
ok_text: None,
|
||||
ok_variant: ButtonVariant::Primary,
|
||||
cancel_text: None,
|
||||
cancel_variant: ButtonVariant::Ghost,
|
||||
cancel_variant: ButtonVariant::Ghost { alt: false },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,12 +450,18 @@ impl RenderOnce for Modal {
|
||||
.top(y)
|
||||
.w(self.width)
|
||||
.when_some(self.max_width, |this, w| this.max_w(w))
|
||||
.child(h_flex().h_4().px_3().justify_center().when_some(
|
||||
self.title,
|
||||
|this, title| {
|
||||
this.h_12().font_semibold().text_center().child(title)
|
||||
},
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.h_4()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.when_some(self.title, |this, title| {
|
||||
this.h_10().font_semibold().text_center().child(title)
|
||||
}),
|
||||
)
|
||||
.when(self.show_close, |this| {
|
||||
this.child(
|
||||
Button::new("close")
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, AnyView, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement,
|
||||
ParentElement as _, Render, Styled, Window,
|
||||
div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement,
|
||||
IntoElement, ParentElement as _, Render, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
|
||||
|
||||
use crate::input::InputState;
|
||||
use crate::modal::Modal;
|
||||
@@ -257,11 +258,29 @@ impl Render for Root {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let base_font_size = cx.theme().font_size;
|
||||
let font_family = cx.theme().font_family.clone();
|
||||
let decorations = window.window_decorations();
|
||||
|
||||
window.set_rem_size(base_font_size);
|
||||
|
||||
window_border().child(
|
||||
div()
|
||||
.id("root")
|
||||
.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)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.right), |el| {
|
||||
el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |el| {
|
||||
el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
}),
|
||||
})
|
||||
.relative()
|
||||
.size_full()
|
||||
.font_family(font_family)
|
||||
|
||||
@@ -9,7 +9,8 @@ use gpui::{
|
||||
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, UniformListScrollHandle, Window,
|
||||
};
|
||||
use theme::{ActiveTheme, ScrollBarMode};
|
||||
use theme::scrollbar_mode::ScrollBarMode;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::AxisExt;
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ pub fn v_flex() -> Div {
|
||||
div().v_flex()
|
||||
}
|
||||
|
||||
/// Returns a `Div` as divider.
|
||||
pub fn divider(cx: &App) -> Div {
|
||||
div().my_2().w_full().h_px().bg(cx.theme().border)
|
||||
}
|
||||
|
||||
macro_rules! font_weight {
|
||||
($fn:ident, $const:ident) => {
|
||||
/// [docs](https://tailwindcss.com/docs/font-weight)
|
||||
|
||||
@@ -233,7 +233,7 @@ impl Element for Switch {
|
||||
.when_some(self.description.clone(), |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.w_3_4()
|
||||
.pr_2()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(description),
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
black, div, px, relative, white, AnyElement, App, ClickEvent, Div, Element, Hsla,
|
||||
InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels, RenderOnce, Rgba,
|
||||
Stateful, StatefulInteractiveElement as _, Style, Styled, Window, WindowControlArea,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, Icon, IconName, InteractiveElementExt as _, Sizable as _};
|
||||
|
||||
const TITLE_BAR_HEIGHT: Pixels = px(34.);
|
||||
#[cfg(target_os = "macos")]
|
||||
const TITLE_BAR_LEFT_PADDING: Pixels = px(80.);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const TITLE_BAR_LEFT_PADDING: Pixels = px(12.);
|
||||
|
||||
type OnCloseWindow = Option<Rc<Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>>>;
|
||||
|
||||
/// TitleBar used to customize the appearance of the title bar.
|
||||
///
|
||||
/// We can put some elements inside the title bar.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TitleBar {
|
||||
base: Stateful<Div>,
|
||||
children: Vec<AnyElement>,
|
||||
on_close_window: OnCloseWindow,
|
||||
}
|
||||
|
||||
impl TitleBar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base: div().id("title-bar"),
|
||||
children: Vec::new(),
|
||||
on_close_window: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
enum ControlIcon {
|
||||
Minimize,
|
||||
Restore,
|
||||
Maximize,
|
||||
Close { on_close_window: OnCloseWindow },
|
||||
}
|
||||
|
||||
impl ControlIcon {
|
||||
fn minimize() -> Self {
|
||||
Self::Minimize
|
||||
}
|
||||
|
||||
fn restore() -> Self {
|
||||
Self::Restore
|
||||
}
|
||||
|
||||
fn maximize() -> Self {
|
||||
Self::Maximize
|
||||
}
|
||||
|
||||
fn close(on_close_window: OnCloseWindow) -> 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 { .. })
|
||||
}
|
||||
|
||||
fn fg(&self, cx: &App) -> Hsla {
|
||||
if cx.theme().mode.is_dark() {
|
||||
white()
|
||||
} else {
|
||||
black()
|
||||
}
|
||||
}
|
||||
|
||||
fn hover_fg(&self, cx: &App) -> Hsla {
|
||||
if self.is_close() || cx.theme().mode.is_dark() {
|
||||
white()
|
||||
} else {
|
||||
black()
|
||||
}
|
||||
}
|
||||
|
||||
fn hover_bg(&self, cx: &App) -> Rgba {
|
||||
if self.is_close() {
|
||||
Rgba {
|
||||
r: 232.0 / 255.0,
|
||||
g: 17.0 / 255.0,
|
||||
b: 32.0 / 255.0,
|
||||
a: 1.0,
|
||||
}
|
||||
} else if cx.theme().mode.is_dark() {
|
||||
Rgba {
|
||||
r: 0.9,
|
||||
g: 0.9,
|
||||
b: 0.9,
|
||||
a: 0.1,
|
||||
}
|
||||
} else {
|
||||
Rgba {
|
||||
r: 0.1,
|
||||
g: 0.1,
|
||||
b: 0.1,
|
||||
a: 0.2,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 fg = self.fg(cx);
|
||||
let hover_fg = self.hover_fg(cx);
|
||||
let hover_bg = self.hover_bg(cx);
|
||||
let icon = self.clone();
|
||||
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()
|
||||
.justify_center()
|
||||
.content_center()
|
||||
.items_center()
|
||||
.text_color(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.hover(|style| style.bg(hover_bg).text_color(hover_fg))
|
||||
.active(|style| style.bg(hover_bg))
|
||||
.child(Icon::new(self.icon()).small())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct WindowControls {
|
||||
on_close_window: OnCloseWindow,
|
||||
}
|
||||
|
||||
impl RenderOnce for WindowControls {
|
||||
fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
|
||||
if cfg!(target_os = "macos") {
|
||||
return div().id("window-controls");
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.id("window-controls")
|
||||
.items_center()
|
||||
.flex_shrink_0()
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_center()
|
||||
.content_stretch()
|
||||
.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 {
|
||||
self.base.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for TitleBar {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TitleBar {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
const HEIGHT: Pixels = px(34.);
|
||||
|
||||
let is_linux = cfg!(target_os = "linux");
|
||||
let is_macos = cfg!(target_os = "macos");
|
||||
|
||||
div().flex_shrink_0().child(
|
||||
self.base
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.h(HEIGHT)
|
||||
.bg(cx.theme().title_bar)
|
||||
.when(window.is_fullscreen(), |this| this.pl(px(12.)))
|
||||
.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())
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.id("bar")
|
||||
.pl(TITLE_BAR_LEFT_PADDING)
|
||||
.when(window.is_fullscreen(), |this| this.pl(px(12.)))
|
||||
.window_control_area(WindowControlArea::Drag)
|
||||
.justify_between()
|
||||
.flex_shrink_0()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.when(is_linux, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.absolute()
|
||||
.size_full()
|
||||
.h_full()
|
||||
.child(TitleBarElement {}),
|
||||
)
|
||||
})
|
||||
.children(self.children),
|
||||
)
|
||||
.child(WindowControls {
|
||||
on_close_window: self.on_close_window,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// A TitleBar Element that can be move the window.
|
||||
pub struct TitleBarElement {}
|
||||
|
||||
impl IntoElement for TitleBarElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TitleBarElement {
|
||||
type PrepaintState = ();
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let style = Style {
|
||||
flex_grow: 1.0,
|
||||
flex_shrink: 1.0,
|
||||
size: gpui::Size {
|
||||
width: relative(1.).into(),
|
||||
height: relative(1.).into(),
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let id = window.request_layout(style, [], cx);
|
||||
|
||||
(id, ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_: gpui::Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
bounds: gpui::Bounds<Pixels>,
|
||||
_: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
use gpui::{MouseButton, MouseMoveEvent, MouseUpEvent};
|
||||
|
||||
window.on_mouse_event(
|
||||
move |ev: &MouseMoveEvent, _, window: &mut Window, _cx: &mut App| {
|
||||
if bounds.contains(&ev.position) && ev.pressed_button == Some(MouseButton::Left) {
|
||||
window.start_window_move();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
window.on_mouse_event(
|
||||
move |ev: &MouseUpEvent, _, window: &mut Window, _cx: &mut App| {
|
||||
if ev.button == MouseButton::Left {
|
||||
window.show_window_menu(ev.position);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,9 @@ use gpui::{
|
||||
HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
|
||||
Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{CLIENT_SIDE_DECORATION_ROUNDING, CLIENT_SIDE_DECORATION_SHADOW};
|
||||
|
||||
pub(crate) const BORDER_SIZE: Pixels = Pixels(1.0);
|
||||
pub(crate) const BORDER_RADIUS: Pixels = Pixels(0.0);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) const SHADOW_SIZE: Pixels = Pixels(0.0);
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) const SHADOW_SIZE: Pixels = Pixels(12.0);
|
||||
const WINDOW_BORDER_WIDTH: Pixels = Pixels(1.0);
|
||||
|
||||
/// Create a new window border.
|
||||
pub fn window_border() -> WindowBorder {
|
||||
@@ -29,7 +24,7 @@ pub fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
|
||||
match window.window_decorations() {
|
||||
Decorations::Server => Edges::all(px(0.0)),
|
||||
Decorations::Client { tiling } => {
|
||||
let mut paddings = Edges::all(SHADOW_SIZE);
|
||||
let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW);
|
||||
if tiling.top {
|
||||
paddings.top = px(0.0);
|
||||
}
|
||||
@@ -62,9 +57,9 @@ impl ParentElement for WindowBorder {
|
||||
}
|
||||
|
||||
impl RenderOnce for WindowBorder {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
let decorations = window.window_decorations();
|
||||
window.set_client_inset(SHADOW_SIZE);
|
||||
window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW);
|
||||
|
||||
div()
|
||||
.id("window-backdrop")
|
||||
@@ -87,7 +82,9 @@ impl RenderOnce for WindowBorder {
|
||||
move |_bounds, hitbox, window, _cx| {
|
||||
let mouse = window.mouse_position();
|
||||
let size = window.window_bounds().get_bounds().size;
|
||||
let Some(edge) = resize_edge(mouse, SHADOW_SIZE, size) else {
|
||||
let Some(edge) =
|
||||
resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
window.set_cursor_style(
|
||||
@@ -113,20 +110,26 @@ impl RenderOnce for WindowBorder {
|
||||
.absolute(),
|
||||
)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(BORDER_RADIUS)
|
||||
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_tl(BORDER_RADIUS)
|
||||
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| div.pt(SHADOW_SIZE))
|
||||
.when(!tiling.bottom, |div| div.pb(SHADOW_SIZE))
|
||||
.when(!tiling.left, |div| div.pl(SHADOW_SIZE))
|
||||
.when(!tiling.right, |div| div.pr(SHADOW_SIZE))
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |div| {
|
||||
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW))
|
||||
.when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW))
|
||||
.when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW))
|
||||
.when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW))
|
||||
.on_mouse_down(MouseButton::Left, move |_, window, _cx| {
|
||||
let size = window.window_bounds().get_bounds().size;
|
||||
let pos = window.mouse_position();
|
||||
|
||||
if let Some(edge) = resize_edge(pos, SHADOW_SIZE, size) {
|
||||
if let Some(edge) = resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) {
|
||||
window.start_window_resize(edge)
|
||||
};
|
||||
}),
|
||||
@@ -137,17 +140,22 @@ impl RenderOnce for WindowBorder {
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.border_color(cx.theme().window_border)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(BORDER_RADIUS)
|
||||
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_tl(BORDER_RADIUS)
|
||||
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| div.border_t(BORDER_SIZE))
|
||||
.when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
|
||||
.when(!tiling.left, |div| div.border_l(BORDER_SIZE))
|
||||
.when(!tiling.right, |div| div.border_r(BORDER_SIZE))
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |div| {
|
||||
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| div.border_t(WINDOW_BORDER_WIDTH))
|
||||
.when(!tiling.bottom, |div| div.border_b(WINDOW_BORDER_WIDTH))
|
||||
.when(!tiling.left, |div| div.border_l(WINDOW_BORDER_WIDTH))
|
||||
.when(!tiling.right, |div| div.border_r(WINDOW_BORDER_WIDTH))
|
||||
.when(!tiling.is_tiled(), |div| {
|
||||
div.shadow(vec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
@@ -156,7 +164,7 @@ impl RenderOnce for WindowBorder {
|
||||
l: 0.,
|
||||
a: 0.3,
|
||||
},
|
||||
blur_radius: SHADOW_SIZE / 2.,
|
||||
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
||||
spread_radius: px(0.),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
}])
|
||||
|
||||
Reference in New Issue
Block a user