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:
reya
2025-08-02 11:37:15 +07:00
committed by GitHub
parent 3cf9dde882
commit c188f12993
43 changed files with 2552 additions and 1790 deletions

View File

@@ -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,
};

View File

@@ -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()

View File

@@ -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!();

View File

@@ -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")

View File

@@ -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)

View File

@@ -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;

View File

@@ -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)

View File

@@ -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),

View File

@@ -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);
}
},
);
}
}

View File

@@ -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)),
}])