mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 10:29:35 +03:00
ui: Refine TitleBar
component (#9415)
This PR continues the refinements to the `TitleBar` component. Here are the notable changes: - `KeyBindingDisplay` and `PlatformStyle` have been unified into a single `PlatformStyle`. - This provides us a consistent way for adapting UI to different platform styles. - `PlatformTitlebar` has been renamed to `TitleBar`. - The `Platform` prefix was irrelevant. - The Windows window controls have been factored out into a separate module and have been componentized. <img width="1283" alt="Screenshot 2024-03-15 at 3 34 38 PM" src="https://github.com/zed-industries/zed/assets/1486634/07da391f-828b-48bf-8849-58863f4ccce7"> > I'm missing the Segoe Fluent Icons font, so that's why the aren't rendering properly. Release Notes: - N/A
This commit is contained in:
parent
dd33330648
commit
c1f1c5e75f
@ -14,7 +14,7 @@ use std::sync::Arc;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
|
||||
ButtonStyle, ContextMenu, Icon, IconButton, IconName, PlatformTitlebar, TintColor, Tooltip,
|
||||
ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, TitleBar, Tooltip,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
||||
@ -58,8 +58,7 @@ impl Render for CollabTitlebarItem {
|
||||
let project_id = self.project.read(cx).remote_id();
|
||||
let workspace = self.workspace.upgrade();
|
||||
|
||||
PlatformTitlebar::new("collab-titlebar")
|
||||
.background(cx.theme().colors().title_bar_background)
|
||||
TitleBar::new("collab-titlebar")
|
||||
// note: on windows titlebar behaviour is handled by the platform implementation
|
||||
.when(cfg!(not(windows)), |this| {
|
||||
this.on_click(|event, cx| {
|
||||
|
@ -29,10 +29,10 @@ pub enum ComponentStory {
|
||||
ListHeader,
|
||||
ListItem,
|
||||
OverflowScroll,
|
||||
PlatformTitlebar,
|
||||
Scroll,
|
||||
Tab,
|
||||
TabBar,
|
||||
TitleBar,
|
||||
ToggleButton,
|
||||
Text,
|
||||
ViewportUnits,
|
||||
@ -61,11 +61,11 @@ impl ComponentStory {
|
||||
Self::ListHeader => cx.new_view(|_| ui::ListHeaderStory).into(),
|
||||
Self::ListItem => cx.new_view(|_| ui::ListItemStory).into(),
|
||||
Self::OverflowScroll => cx.new_view(|_| crate::stories::OverflowScrollStory).into(),
|
||||
Self::PlatformTitlebar => cx.new_view(|_| ui::PlatformTitlebarStory).into(),
|
||||
Self::Scroll => ScrollStory::view(cx).into(),
|
||||
Self::Text => TextStory::view(cx).into(),
|
||||
Self::Tab => cx.new_view(|_| ui::TabStory).into(),
|
||||
Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(),
|
||||
Self::TitleBar => cx.new_view(|_| ui::TitleBarStory).into(),
|
||||
Self::ToggleButton => cx.new_view(|_| ui::ToggleButtonStory).into(),
|
||||
Self::ViewportUnits => cx.new_view(|_| crate::stories::ViewportUnitsStory).into(),
|
||||
Self::Picker => PickerStory::new(cx).into(),
|
||||
|
@ -9,13 +9,13 @@ mod indicator;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list;
|
||||
mod platform_titlebar;
|
||||
mod popover;
|
||||
mod popover_menu;
|
||||
mod right_click_menu;
|
||||
mod stack;
|
||||
mod tab;
|
||||
mod tab_bar;
|
||||
mod title_bar;
|
||||
mod tooltip;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
@ -32,13 +32,13 @@ pub use indicator::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list::*;
|
||||
pub use platform_titlebar::*;
|
||||
pub use popover::*;
|
||||
pub use popover_menu::*;
|
||||
pub use right_click_menu::*;
|
||||
pub use stack::*;
|
||||
pub use tab::*;
|
||||
pub use tab_bar::*;
|
||||
pub use title_bar::*;
|
||||
pub use tooltip::*;
|
||||
|
||||
#[cfg(feature = "stories")]
|
||||
|
@ -1,30 +1,6 @@
|
||||
use crate::{h_flex, prelude::*, Icon, IconName, IconSize};
|
||||
use gpui::{relative, Action, FocusHandle, IntoElement, Keystroke};
|
||||
|
||||
/// The way a [`KeyBinding`] should be displayed.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum KeyBindingDisplay {
|
||||
/// Display in macOS style.
|
||||
Mac,
|
||||
/// Display in Linux style.
|
||||
Linux,
|
||||
/// Display in Windows style.
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl KeyBindingDisplay {
|
||||
/// Returns the [`KeyBindingDisplay`] for the current platform.
|
||||
pub const fn platform() -> Self {
|
||||
if cfg!(target_os = "linux") {
|
||||
KeyBindingDisplay::Linux
|
||||
} else if cfg!(target_os = "windows") {
|
||||
KeyBindingDisplay::Windows
|
||||
} else {
|
||||
KeyBindingDisplay::Mac
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement, Clone)]
|
||||
pub struct KeyBinding {
|
||||
/// A keybinding consists of a key and a set of modifier keys.
|
||||
@ -33,8 +9,8 @@ pub struct KeyBinding {
|
||||
/// This should always contain at least one element.
|
||||
key_binding: gpui::KeyBinding,
|
||||
|
||||
/// How keybindings should be displayed.
|
||||
display: KeyBindingDisplay,
|
||||
/// The [`PlatformStyle`] to use when displaying this keybinding.
|
||||
platform_style: PlatformStyle,
|
||||
}
|
||||
|
||||
impl KeyBinding {
|
||||
@ -76,13 +52,13 @@ impl KeyBinding {
|
||||
pub fn new(key_binding: gpui::KeyBinding) -> Self {
|
||||
Self {
|
||||
key_binding,
|
||||
display: KeyBindingDisplay::platform(),
|
||||
platform_style: PlatformStyle::platform(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets how this [`KeyBinding`] should be displayed.
|
||||
pub fn display(mut self, display: KeyBindingDisplay) -> Self {
|
||||
self.display = display;
|
||||
/// Sets the [`PlatformStyle`] for this [`KeyBinding`].
|
||||
pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self {
|
||||
self.platform_style = platform_style;
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -97,43 +73,49 @@ impl RenderOnce for KeyBinding {
|
||||
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.map(|el| match self.display {
|
||||
KeyBindingDisplay::Mac => el.gap_0p5(),
|
||||
KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => el,
|
||||
.map(|el| match self.platform_style {
|
||||
PlatformStyle::Mac => el.gap_0p5(),
|
||||
PlatformStyle::Linux | PlatformStyle::Windows => el,
|
||||
})
|
||||
.p_0p5()
|
||||
.rounded_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when(keystroke.modifiers.function, |el| match self.display {
|
||||
KeyBindingDisplay::Mac => el.child(Key::new("fn")),
|
||||
KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
|
||||
el.child(Key::new("Fn")).child(Key::new("+"))
|
||||
.when(keystroke.modifiers.function, |el| {
|
||||
match self.platform_style {
|
||||
PlatformStyle::Mac => el.child(Key::new("fn")),
|
||||
PlatformStyle::Linux | PlatformStyle::Windows => {
|
||||
el.child(Key::new("Fn")).child(Key::new("+"))
|
||||
}
|
||||
}
|
||||
})
|
||||
.when(keystroke.modifiers.control, |el| match self.display {
|
||||
KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Control)),
|
||||
KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
|
||||
el.child(Key::new("Ctrl")).child(Key::new("+"))
|
||||
.when(keystroke.modifiers.control, |el| {
|
||||
match self.platform_style {
|
||||
PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Control)),
|
||||
PlatformStyle::Linux | PlatformStyle::Windows => {
|
||||
el.child(Key::new("Ctrl")).child(Key::new("+"))
|
||||
}
|
||||
}
|
||||
})
|
||||
.when(keystroke.modifiers.alt, |el| match self.display {
|
||||
KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Option)),
|
||||
KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
|
||||
.when(keystroke.modifiers.alt, |el| match self.platform_style {
|
||||
PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Option)),
|
||||
PlatformStyle::Linux | PlatformStyle::Windows => {
|
||||
el.child(Key::new("Alt")).child(Key::new("+"))
|
||||
}
|
||||
})
|
||||
.when(keystroke.modifiers.command, |el| match self.display {
|
||||
KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Command)),
|
||||
KeyBindingDisplay::Linux => {
|
||||
el.child(Key::new("Super")).child(Key::new("+"))
|
||||
}
|
||||
KeyBindingDisplay::Windows => {
|
||||
el.child(Key::new("Win")).child(Key::new("+"))
|
||||
.when(keystroke.modifiers.command, |el| {
|
||||
match self.platform_style {
|
||||
PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Command)),
|
||||
PlatformStyle::Linux => {
|
||||
el.child(Key::new("Super")).child(Key::new("+"))
|
||||
}
|
||||
PlatformStyle::Windows => {
|
||||
el.child(Key::new("Win")).child(Key::new("+"))
|
||||
}
|
||||
}
|
||||
})
|
||||
.when(keystroke.modifiers.shift, |el| match self.display {
|
||||
KeyBindingDisplay::Mac => el.child(KeyIcon::new(IconName::Shift)),
|
||||
KeyBindingDisplay::Linux | KeyBindingDisplay::Windows => {
|
||||
.when(keystroke.modifiers.shift, |el| match self.platform_style {
|
||||
PlatformStyle::Mac => el.child(KeyIcon::new(IconName::Shift)),
|
||||
PlatformStyle::Linux | PlatformStyle::Windows => {
|
||||
el.child(Key::new("Shift")).child(Key::new("+"))
|
||||
}
|
||||
})
|
||||
|
@ -1,215 +0,0 @@
|
||||
use gpui::{transparent_black, AnyElement, Fill, Interactivity, Rgba, Stateful, WindowAppearance};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
pub enum PlatformStyle {
|
||||
Linux,
|
||||
Windows,
|
||||
MacOs,
|
||||
}
|
||||
|
||||
pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
|
||||
(1.75 * cx.rem_size()).max(px(32.))
|
||||
}
|
||||
|
||||
impl PlatformStyle {
|
||||
pub fn platform() -> Self {
|
||||
if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Self::MacOs
|
||||
} else {
|
||||
Self::Linux
|
||||
}
|
||||
}
|
||||
|
||||
pub fn windows(&self) -> bool {
|
||||
matches!(self, Self::Windows)
|
||||
}
|
||||
|
||||
pub fn macos(&self) -> bool {
|
||||
matches!(self, Self::MacOs)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct PlatformTitlebar {
|
||||
platform: PlatformStyle,
|
||||
background: Fill,
|
||||
content: Stateful<Div>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl PlatformTitlebar {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
platform: PlatformStyle::platform(),
|
||||
background: transparent_black().into(),
|
||||
content: div().id(id.into()),
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the platform style.
|
||||
pub fn platform_style(mut self, style: PlatformStyle) -> Self {
|
||||
self.platform = style;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the background color of the titlebar.
|
||||
pub fn background<F>(mut self, fill: F) -> Self
|
||||
where
|
||||
F: Into<Fill>,
|
||||
Self: Sized,
|
||||
{
|
||||
self.background = fill.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn top_padding(&self, cx: &WindowContext) -> Pixels {
|
||||
if self.platform.windows() && cx.is_maximized() {
|
||||
// todo(windows): get padding from win32 api, need HWND from window context somehow
|
||||
// should be GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) * 2
|
||||
px(8.0)
|
||||
} else {
|
||||
px(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn windows_caption_button_width(_cx: &WindowContext) -> Pixels {
|
||||
// todo(windows): get padding from win32 api, need HWND from window context somehow
|
||||
// should be GetSystemMetricsForDpi(SM_CXSIZE, dpi)
|
||||
px(36.0)
|
||||
}
|
||||
|
||||
fn render_window_controls_right(&self, cx: &mut WindowContext) -> impl Element {
|
||||
if !self.platform.windows() {
|
||||
return div().id("caption-buttons-windows");
|
||||
}
|
||||
|
||||
let button_height = titlebar_height(cx) - self.top_padding(cx);
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
fn windows_caption_button(
|
||||
id: &'static str,
|
||||
icon_text: &'static str,
|
||||
hover_color: Rgba,
|
||||
cx: &WindowContext,
|
||||
) -> Stateful<Div> {
|
||||
let mut active_color = hover_color;
|
||||
active_color.a *= 0.2;
|
||||
h_flex()
|
||||
.id(id)
|
||||
.h_full()
|
||||
.justify_center()
|
||||
.content_center()
|
||||
.items_center()
|
||||
.w(PlatformTitlebar::windows_caption_button_width(cx))
|
||||
.hover(|style| style.bg(hover_color))
|
||||
.active(|style| style.bg(active_color))
|
||||
.child(icon_text)
|
||||
}
|
||||
|
||||
const MINIMIZE_ICON: &str = "\u{e921}";
|
||||
const RESTORE_ICON: &str = "\u{e923}";
|
||||
const MAXIMIZE_ICON: &str = "\u{e922}";
|
||||
const CLOSE_ICON: &str = "\u{e8bb}";
|
||||
|
||||
div()
|
||||
.id("caption-buttons-windows")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_center()
|
||||
.content_stretch()
|
||||
.max_h(button_height)
|
||||
.min_h(button_height)
|
||||
.font("Segoe Fluent Icons")
|
||||
.text_size(px(10.0))
|
||||
.children(vec![
|
||||
windows_caption_button("minimize", MINIMIZE_ICON, button_hover_color, cx),
|
||||
windows_caption_button(
|
||||
"maximize",
|
||||
if cx.is_maximized() {
|
||||
RESTORE_ICON
|
||||
} else {
|
||||
MAXIMIZE_ICON
|
||||
},
|
||||
button_hover_color,
|
||||
cx,
|
||||
),
|
||||
windows_caption_button("close", CLOSE_ICON, close_button_hover_color, cx),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for PlatformTitlebar {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let titlebar_height = titlebar_height(cx);
|
||||
let titlebar_top_padding = self.top_padding(cx);
|
||||
let window_controls_right = self.render_window_controls_right(cx);
|
||||
|
||||
h_flex()
|
||||
.id("titlebar")
|
||||
.w_full()
|
||||
.pt(titlebar_top_padding)
|
||||
.h(titlebar_height)
|
||||
.map(|this| {
|
||||
if cx.is_fullscreen() {
|
||||
this.pl_2()
|
||||
} else if self.platform.macos() {
|
||||
// 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.
|
||||
this.pl(px(80.))
|
||||
} else {
|
||||
this.pl_2()
|
||||
}
|
||||
})
|
||||
.bg(self.background)
|
||||
.content_stretch()
|
||||
.child(
|
||||
self.content
|
||||
.id("titlebar-content")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.children(self.children),
|
||||
)
|
||||
.child(window_controls_right)
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for PlatformTitlebar {
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
self.content.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulInteractiveElement for PlatformTitlebar {}
|
||||
|
||||
impl ParentElement for PlatformTitlebar {
|
||||
fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
@ -10,9 +10,9 @@ mod label;
|
||||
mod list;
|
||||
mod list_header;
|
||||
mod list_item;
|
||||
mod platform_titlebar;
|
||||
mod tab;
|
||||
mod tab_bar;
|
||||
mod title_bar;
|
||||
mod toggle_button;
|
||||
|
||||
pub use avatar::*;
|
||||
@ -27,7 +27,7 @@ pub use label::*;
|
||||
pub use list::*;
|
||||
pub use list_header::*;
|
||||
pub use list_item::*;
|
||||
pub use platform_titlebar::*;
|
||||
pub use tab::*;
|
||||
pub use tab_bar::*;
|
||||
pub use title_bar::*;
|
||||
pub use toggle_button::*;
|
||||
|
@ -3,7 +3,7 @@ use gpui::Render;
|
||||
use itertools::Itertools;
|
||||
use story::{Story, StoryContainer};
|
||||
|
||||
use crate::{prelude::*, KeyBinding, KeyBindingDisplay};
|
||||
use crate::{prelude::*, KeyBinding};
|
||||
|
||||
pub struct KeybindingStory;
|
||||
|
||||
@ -59,18 +59,22 @@ impl Render for KeybindingStory {
|
||||
.child(KeyBinding::new(binding("ctrl-a shift-z")))
|
||||
.child(KeyBinding::new(binding("fn-s")))
|
||||
.child(Story::label("Single Key with All Modifiers (Linux)"))
|
||||
.child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).display(KeyBindingDisplay::Linux))
|
||||
.child(
|
||||
KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).platform_style(PlatformStyle::Linux),
|
||||
)
|
||||
.child(Story::label("Chord (Linux)"))
|
||||
.child(KeyBinding::new(binding("a z")).display(KeyBindingDisplay::Linux))
|
||||
.child(KeyBinding::new(binding("a z")).platform_style(PlatformStyle::Linux))
|
||||
.child(Story::label("Chord with Modifier (Linux)"))
|
||||
.child(KeyBinding::new(binding("ctrl-a shift-z")).display(KeyBindingDisplay::Linux))
|
||||
.child(KeyBinding::new(binding("fn-s")).display(KeyBindingDisplay::Linux))
|
||||
.child(KeyBinding::new(binding("ctrl-a shift-z")).platform_style(PlatformStyle::Linux))
|
||||
.child(KeyBinding::new(binding("fn-s")).platform_style(PlatformStyle::Linux))
|
||||
.child(Story::label("Single Key with All Modifiers (Windows)"))
|
||||
.child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).display(KeyBindingDisplay::Windows))
|
||||
.child(
|
||||
KeyBinding::new(binding("ctrl-alt-cmd-shift-z")).platform_style(PlatformStyle::Windows),
|
||||
)
|
||||
.child(Story::label("Chord (Windows)"))
|
||||
.child(KeyBinding::new(binding("a z")).display(KeyBindingDisplay::Windows))
|
||||
.child(KeyBinding::new(binding("a z")).platform_style(PlatformStyle::Windows))
|
||||
.child(Story::label("Chord with Modifier (Windows)"))
|
||||
.child(KeyBinding::new(binding("ctrl-a shift-z")).display(KeyBindingDisplay::Windows))
|
||||
.child(KeyBinding::new(binding("fn-s")).display(KeyBindingDisplay::Windows))
|
||||
.child(KeyBinding::new(binding("ctrl-a shift-z")).platform_style(PlatformStyle::Windows))
|
||||
.child(KeyBinding::new(binding("fn-s")).platform_style(PlatformStyle::Windows))
|
||||
}
|
||||
}
|
||||
|
@ -1,46 +0,0 @@
|
||||
use gpui::Render;
|
||||
use story::{StoryContainer, StoryItem, StorySection};
|
||||
|
||||
use crate::{prelude::*, PlatformStyle, PlatformTitlebar};
|
||||
|
||||
pub struct PlatformTitlebarStory;
|
||||
|
||||
impl Render for PlatformTitlebarStory {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
StoryContainer::new(
|
||||
"Platform Titlebar",
|
||||
"crates/ui/src/components/stories/platform_titlebar.rs",
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default (macOS)",
|
||||
PlatformTitlebar::new("macos").platform_style(PlatformStyle::MacOs),
|
||||
)
|
||||
.description("")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default (Linux)",
|
||||
PlatformTitlebar::new("linux").platform_style(PlatformStyle::Linux),
|
||||
)
|
||||
.description("")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default (Windows)",
|
||||
PlatformTitlebar::new("windows").platform_style(PlatformStyle::Windows),
|
||||
)
|
||||
.description("")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
}
|
56
crates/ui/src/components/stories/title_bar.rs
Normal file
56
crates/ui/src/components/stories/title_bar.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use gpui::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")
|
||||
.platform_style(PlatformStyle::Mac)
|
||||
.map(add_sample_children),
|
||||
)
|
||||
.description("")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default (Linux)",
|
||||
TitleBar::new("linux")
|
||||
.platform_style(PlatformStyle::Linux)
|
||||
.map(add_sample_children),
|
||||
)
|
||||
.description("")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
StorySection::new().child(
|
||||
StoryItem::new(
|
||||
"Default (Windows)",
|
||||
TitleBar::new("windows")
|
||||
.platform_style(PlatformStyle::Windows)
|
||||
.map(add_sample_children),
|
||||
)
|
||||
.description("")
|
||||
.usage(""),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
}
|
4
crates/ui/src/components/title_bar.rs
Normal file
4
crates/ui/src/components/title_bar.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod title_bar;
|
||||
mod windows_window_controls;
|
||||
|
||||
pub use title_bar::*;
|
96
crates/ui/src/components/title_bar/title_bar.rs
Normal file
96
crates/ui/src/components/title_bar/title_bar.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use gpui::{AnyElement, Interactivity, Stateful};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
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]>,
|
||||
}
|
||||
|
||||
impl TitleBar {
|
||||
pub fn height(cx: &mut WindowContext) -> Pixels {
|
||||
(1.75 * cx.rem_size()).max(px(32.))
|
||||
}
|
||||
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
platform_style: PlatformStyle::platform(),
|
||||
content: div().id(id.into()),
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the platform style.
|
||||
pub fn platform_style(mut self, style: PlatformStyle) -> Self {
|
||||
self.platform_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
fn top_padding(&self, cx: &WindowContext) -> Pixels {
|
||||
if self.platform_style == PlatformStyle::Windows && cx.is_maximized() {
|
||||
// todo(windows): get padding from win32 api, need HWND from window context somehow
|
||||
// should be GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) * 2
|
||||
px(8.)
|
||||
} else {
|
||||
px(0.)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 Iterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TitleBar {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let height = Self::height(cx);
|
||||
let top_padding = self.top_padding(cx);
|
||||
|
||||
h_flex()
|
||||
.id("titlebar")
|
||||
.w_full()
|
||||
.pt(top_padding)
|
||||
.h(height)
|
||||
.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.
|
||||
this.pl(px(80.))
|
||||
} 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, |title_bar| {
|
||||
let button_height = Self::height(cx) - top_padding;
|
||||
|
||||
title_bar.child(WindowsWindowControls::new(button_height))
|
||||
})
|
||||
}
|
||||
}
|
127
crates/ui/src/components/title_bar/windows_window_controls.rs
Normal file
127
crates/ui/src/components/title_bar/windows_window_controls.rs
Normal file
@ -0,0 +1,127 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_center()
|
||||
.content_stretch()
|
||||
.max_h(self.button_height)
|
||||
.min_h(self.button_height)
|
||||
.font("Segoe Fluent Icons")
|
||||
.text_size(px(10.0))
|
||||
.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): get padding from win32 api, need HWND from window context somehow
|
||||
// should be GetSystemMetricsForDpi(SM_CXSIZE, dpi)
|
||||
let width = px(36.0);
|
||||
|
||||
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(match self.icon {
|
||||
WindowsCaptionButtonIcon::Minimize => "\u{e921}",
|
||||
WindowsCaptionButtonIcon::Restore => "\u{e923}",
|
||||
WindowsCaptionButtonIcon::Maximize => "\u{e922}",
|
||||
WindowsCaptionButtonIcon::Close => "\u{e8bb}",
|
||||
})
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ pub use crate::clickable::*;
|
||||
pub use crate::disableable::*;
|
||||
pub use crate::fixed::*;
|
||||
pub use crate::selectable::*;
|
||||
pub use crate::styles::{rems_from_px, vh, vw};
|
||||
pub use crate::styles::{rems_from_px, vh, vw, PlatformStyle};
|
||||
pub use crate::visible_on_hover::*;
|
||||
pub use crate::{h_flex, v_flex};
|
||||
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
|
||||
|
@ -1,9 +1,11 @@
|
||||
mod color;
|
||||
mod elevation;
|
||||
mod platform;
|
||||
mod typography;
|
||||
mod units;
|
||||
|
||||
pub use color::*;
|
||||
pub use elevation::*;
|
||||
pub use platform::*;
|
||||
pub use typography::*;
|
||||
pub use units::*;
|
||||
|
25
crates/ui/src/styles/platform.rs
Normal file
25
crates/ui/src/styles/platform.rs
Normal file
@ -0,0 +1,25 @@
|
||||
/// The platform style to use when rendering UI.
|
||||
///
|
||||
/// This can be used to abstract over platform differences.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
pub enum PlatformStyle {
|
||||
/// Display in macOS style.
|
||||
Mac,
|
||||
/// Display in Linux style.
|
||||
Linux,
|
||||
/// Display in Windows style.
|
||||
Windows,
|
||||
}
|
||||
|
||||
impl PlatformStyle {
|
||||
/// Returns the [`PlatformStyle`] for the current platform.
|
||||
pub const fn platform() -> Self {
|
||||
if cfg!(target_os = "linux") {
|
||||
Self::Linux
|
||||
} else if cfg!(target_os = "windows") {
|
||||
Self::Windows
|
||||
} else {
|
||||
Self::Mac
|
||||
}
|
||||
}
|
||||
}
|
@ -4809,7 +4809,7 @@ impl Element for DisconnectedOverlay {
|
||||
.bg(background)
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top(ui::titlebar_height(cx))
|
||||
.top(ui::TitleBar::height(cx))
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
|
Loading…
Reference in New Issue
Block a user