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:
parent
7652a8ae23
commit
0b57df5deb
22 changed files with 756 additions and 655 deletions
|
@ -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::*;
|
||||
|
||||
|
|
54
crates/ui/src/components/facepile.rs
Normal file
54
crates/ui/src/components/facepile.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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}",
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue