Extract title_bar crate (#13597)

This PR extracts a singular title bar (`title_bar::TitleBar`) from
`ui::TitleBar` and
`collab_ui::collab_titlebar_item::CollabTitlebarItem`.

This is a first step towards organizing title bar things into one place,
and standardizing platform titlebar/window control implementations.

Release Notes:

- N/A
This commit is contained in:
Nate Butler 2024-06-27 19:14:13 -04:00 committed by GitHub
parent 7652a8ae23
commit 0b57df5deb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 756 additions and 655 deletions

View file

@ -5,6 +5,7 @@ mod context_menu;
mod disclosure;
mod divider;
mod dropdown_menu;
mod facepile;
mod icon;
mod indicator;
mod keybinding;
@ -19,7 +20,6 @@ mod setting;
mod stack;
mod tab;
mod tab_bar;
mod title_bar;
mod tool_strip;
mod tooltip;
@ -33,6 +33,7 @@ pub use context_menu::*;
pub use disclosure::*;
pub use divider::*;
use dropdown_menu::*;
pub use facepile::*;
pub use icon::*;
pub use indicator::*;
pub use keybinding::*;
@ -47,7 +48,6 @@ pub use setting::*;
pub use stack::*;
pub use tab::*;
pub use tab_bar::*;
pub use title_bar::*;
pub use tool_strip::*;
pub use tooltip::*;

View file

@ -0,0 +1,54 @@
use crate::prelude::*;
use gpui::AnyElement;
use smallvec::SmallVec;
/// A facepile is a collection of faces stacked horizontally
/// always with the leftmost face on top and descending in z-index
///
/// Facepiles are used to display a group of people or things,
/// such as a list of participants in a collaboration session.
#[derive(IntoElement)]
pub struct Facepile {
base: Div,
faces: SmallVec<[AnyElement; 2]>,
}
impl Facepile {
pub fn empty() -> Self {
Self::new(SmallVec::new())
}
pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self {
Self { base: div(), faces }
}
}
impl RenderOnce for Facepile {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
// Lay the faces out in reverse so they overlap in the desired order (left to right, front to back)
self.base
.flex()
.flex_row_reverse()
.items_center()
.justify_start()
.children(
self.faces
.into_iter()
.enumerate()
.rev()
.map(|(ix, player)| div().when(ix > 0, |div| div.ml_neg_1()).child(player)),
)
}
}
impl ParentElement for Facepile {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.faces.extend(elements);
}
}
impl Styled for Facepile {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
}
}

View file

@ -13,7 +13,6 @@ mod list_item;
mod setting;
mod tab;
mod tab_bar;
mod title_bar;
mod toggle_button;
mod tool_strip;
@ -32,6 +31,5 @@ pub use list_item::*;
pub use setting::*;
pub use tab::*;
pub use tab_bar::*;
pub use title_bar::*;
pub use toggle_button::*;
pub use tool_strip::*;

View file

@ -1,56 +0,0 @@
use gpui::{NoAction, Render};
use story::{StoryContainer, StoryItem, StorySection};
use crate::{prelude::*, PlatformStyle, TitleBar};
pub struct TitleBarStory;
impl Render for TitleBarStory {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
fn add_sample_children(titlebar: TitleBar) -> TitleBar {
titlebar
.child(div().size_2().bg(gpui::red()))
.child(div().size_2().bg(gpui::blue()))
.child(div().size_2().bg(gpui::green()))
}
StoryContainer::new("TitleBar", "crates/ui/src/components/stories/title_bar.rs")
.child(
StorySection::new().child(
StoryItem::new(
"Default (macOS)",
TitleBar::new("macos", Box::new(NoAction))
.platform_style(PlatformStyle::Mac)
.map(add_sample_children),
)
.description("")
.usage(""),
),
)
.child(
StorySection::new().child(
StoryItem::new(
"Default (Linux)",
TitleBar::new("linux", Box::new(NoAction))
.platform_style(PlatformStyle::Linux)
.map(add_sample_children),
)
.description("")
.usage(""),
),
)
.child(
StorySection::new().child(
StoryItem::new(
"Default (Windows)",
TitleBar::new("windows", Box::new(NoAction))
.platform_style(PlatformStyle::Windows)
.map(add_sample_children),
)
.description("")
.usage(""),
),
)
.into_element()
}
}

View file

@ -1,145 +0,0 @@
use gpui::{prelude::*, Action, Rgba, WindowAppearance};
use crate::prelude::*;
#[derive(IntoElement)]
pub struct LinuxWindowControls {
button_height: Pixels,
close_window_action: Box<dyn Action>,
}
impl LinuxWindowControls {
pub fn new(button_height: Pixels, close_window_action: Box<dyn Action>) -> Self {
Self {
button_height,
close_window_action,
}
}
}
impl RenderOnce for LinuxWindowControls {
fn render(self, cx: &mut WindowContext) -> 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 = match cx.appearance() {
WindowAppearance::Light | WindowAppearance::VibrantLight => Rgba {
r: 0.1,
g: 0.1,
b: 0.1,
a: 0.2,
},
WindowAppearance::Dark | WindowAppearance::VibrantDark => Rgba {
r: 0.9,
g: 0.9,
b: 0.9,
a: 0.1,
},
};
div()
.id("linux-window-controls")
.flex()
.flex_row()
.justify_center()
.content_stretch()
.max_h(self.button_height)
.min_h(self.button_height)
.child(TitlebarButton::new(
"minimize",
TitlebarButtonType::Minimize,
button_hover_color,
self.close_window_action.boxed_clone(),
))
.child(TitlebarButton::new(
"maximize-or-restore",
if cx.is_maximized() {
TitlebarButtonType::Restore
} else {
TitlebarButtonType::Maximize
},
button_hover_color,
self.close_window_action.boxed_clone(),
))
.child(TitlebarButton::new(
"close",
TitlebarButtonType::Close,
close_button_hover_color,
self.close_window_action,
))
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum TitlebarButtonType {
Minimize,
Restore,
Maximize,
Close,
}
#[derive(IntoElement)]
struct TitlebarButton {
id: ElementId,
icon: TitlebarButtonType,
hover_background_color: Rgba,
close_window_action: Box<dyn Action>,
}
impl TitlebarButton {
pub fn new(
id: impl Into<ElementId>,
icon: TitlebarButtonType,
hover_background_color: Rgba,
close_window_action: Box<dyn Action>,
) -> Self {
Self {
id: id.into(),
icon,
hover_background_color,
close_window_action,
}
}
}
impl RenderOnce for TitlebarButton {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let width = px(36.);
h_flex()
.id(self.id)
.justify_center()
.content_center()
.w(width)
.h_full()
.hover(|style| style.bg(self.hover_background_color))
.active(|style| {
let mut active_color = self.hover_background_color;
active_color.a *= 0.2;
style.bg(active_color)
})
.child(Icon::new(match self.icon {
TitlebarButtonType::Minimize => IconName::Dash,
TitlebarButtonType::Restore => IconName::Minimize,
TitlebarButtonType::Maximize => IconName::Maximize,
TitlebarButtonType::Close => IconName::Close,
}))
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_click(move |_, cx| {
cx.stop_propagation();
match self.icon {
TitlebarButtonType::Minimize => cx.minimize_window(),
TitlebarButtonType::Restore => cx.zoom_window(),
TitlebarButtonType::Maximize => cx.zoom_window(),
TitlebarButtonType::Close => {
cx.dispatch_action(self.close_window_action.boxed_clone())
}
}
})
}
}

View file

@ -1,135 +0,0 @@
use gpui::{Action, AnyElement, Interactivity, Stateful};
use smallvec::SmallVec;
use crate::components::title_bar::linux_window_controls::LinuxWindowControls;
use crate::components::title_bar::windows_window_controls::WindowsWindowControls;
use crate::prelude::*;
#[derive(IntoElement)]
pub struct TitleBar {
platform_style: PlatformStyle,
content: Stateful<Div>,
children: SmallVec<[AnyElement; 2]>,
close_window_action: Box<dyn Action>,
}
impl TitleBar {
#[cfg(not(target_os = "windows"))]
pub fn height(cx: &mut WindowContext) -> Pixels {
(1.75 * cx.rem_size()).max(px(34.))
}
#[cfg(target_os = "windows")]
pub fn height(_cx: &mut WindowContext) -> Pixels {
// todo(windows) instead of hard coded size report the actual size to the Windows platform API
px(32.)
}
#[cfg(not(target_os = "windows"))]
fn top_padding(_cx: &WindowContext) -> Pixels {
px(0.)
}
#[cfg(target_os = "windows")]
fn top_padding(cx: &WindowContext) -> Pixels {
use windows::Win32::UI::{
HiDpi::GetSystemMetricsForDpi,
WindowsAndMessaging::{SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI},
};
// This top padding is not dependent on the title bar style and is instead a quirk of maximized windows on Windows:
// https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543
let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
if cx.is_maximized() {
px((padding * 2) as f32)
} else {
px(0.)
}
}
pub fn new(id: impl Into<ElementId>, close_window_action: Box<dyn Action>) -> Self {
Self {
platform_style: PlatformStyle::platform(),
content: div().id(id.into()),
children: SmallVec::new(),
close_window_action,
}
}
/// Sets the platform style.
pub fn platform_style(mut self, style: PlatformStyle) -> Self {
self.platform_style = style;
self
}
}
impl InteractiveElement for TitleBar {
fn interactivity(&mut self) -> &mut Interactivity {
self.content.interactivity()
}
}
impl StatefulInteractiveElement for TitleBar {}
impl ParentElement for TitleBar {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl RenderOnce for TitleBar {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let height = Self::height(cx);
h_flex()
.id("titlebar")
.w_full()
.pt(Self::top_padding(cx))
.h(height + Self::top_padding(cx))
.map(|this| {
if cx.is_fullscreen() {
this.pl_2()
} else if self.platform_style == PlatformStyle::Mac {
// 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.
this.pl(px(71.))
} else {
this.pl_2()
}
})
.bg(cx.theme().colors().title_bar_background)
.content_stretch()
.child(
self.content
.id("titlebar-content")
.flex()
.flex_row()
.justify_between()
.w_full()
.children(self.children),
)
.when(
self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
|title_bar| title_bar.child(WindowsWindowControls::new(height)),
)
.when(
self.platform_style == PlatformStyle::Linux
&& !cx.is_fullscreen()
&& cx.should_render_window_controls(),
|title_bar| {
title_bar
.child(LinuxWindowControls::new(height, self.close_window_action))
.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
cx.show_window_menu(ev.position)
})
.on_mouse_move(move |ev, cx| {
if ev.dragging() {
cx.start_system_move();
}
})
},
)
}
}

View file

@ -1,148 +0,0 @@
use gpui::{prelude::*, Rgba, WindowAppearance};
use crate::prelude::*;
#[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, cx: &mut WindowContext) -> 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 = match cx.appearance() {
WindowAppearance::Light | WindowAppearance::VibrantLight => Rgba {
r: 0.1,
g: 0.1,
b: 0.1,
a: 0.2,
},
WindowAppearance::Dark | WindowAppearance::VibrantDark => Rgba {
r: 0.9,
g: 0.9,
b: 0.9,
a: 0.1,
},
};
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,
))
.child(WindowsCaptionButton::new(
"maximize-or-restore",
if cx.is_maximized() {
WindowsCaptionButtonIcon::Restore
} else {
WindowsCaptionButtonIcon::Maximize
},
button_hover_color,
))
.child(WindowsCaptionButton::new(
"close",
WindowsCaptionButtonIcon::Close,
close_button_hover_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: Rgba,
}
impl WindowsCaptionButton {
pub fn new(
id: impl Into<ElementId>,
icon: WindowsCaptionButtonIcon,
hover_background_color: Rgba,
) -> Self {
Self {
id: id.into(),
icon,
hover_background_color,
}
}
}
impl RenderOnce for WindowsCaptionButton {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
// todo(windows) report this width to the Windows platform API
// NOTE: this is intentionally hard coded. An option to use the 'native' size
// could be added when the width is reported to the Windows platform API
// as this could change between future Windows versions.
let width = px(36.);
h_flex()
.id(self.id)
.justify_center()
.content_center()
.w(width)
.h_full()
.text_size(px(10.0))
.hover(|style| style.bg(self.hover_background_color))
.active(|style| {
let mut active_color = self.hover_background_color;
active_color.a *= 0.2;
style.bg(active_color)
})
.child(match self.icon {
WindowsCaptionButtonIcon::Minimize => "\u{e921}",
WindowsCaptionButtonIcon::Restore => "\u{e923}",
WindowsCaptionButtonIcon::Maximize => "\u{e922}",
WindowsCaptionButtonIcon::Close => "\u{e8bb}",
})
}
}