Cargo.lock 🔗
@@ -13607,6 +13607,7 @@ dependencies = [
"serde",
"settings",
"theme",
+ "title_bar",
"ui",
"util",
"workspace",
Smit Barmase created
Closes #30513
- Abstract away common wrapper component to `platform_title_bar`.
- Use it in both zed and rules library.
- For rules library, keep traffic like only style for macOS, and add
custom title bar for Linux and Windows.
Release Notes:
- Added way to minimize, maximize, and close the rules library window
for Linux.
Cargo.lock | 1
crates/rules_library/Cargo.toml | 1
crates/rules_library/src/rules_library.rs | 173 ++++++++------
crates/title_bar/src/platform_title_bar.rs | 169 ++++++++++++++
crates/title_bar/src/title_bar.rs | 275 +++++------------------
5 files changed, 339 insertions(+), 280 deletions(-)
@@ -13607,6 +13607,7 @@ dependencies = [
"serde",
"settings",
"theme",
+ "title_bar",
"ui",
"util",
"workspace",
@@ -27,6 +27,7 @@ rope.workspace = true
serde.workspace = true
settings.workspace = true
theme.workspace = true
+title_bar.workspace = true
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
@@ -20,12 +20,13 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
use theme::ThemeSettings;
+use title_bar::platform_title_bar::PlatformTitleBar;
use ui::{
Context, IconButtonShape, KeyBinding, ListItem, ListItemSpacing, ParentElement, Render,
SharedString, Styled, Tooltip, Window, div, prelude::*,
};
use util::{ResultExt, TryFutureExt};
-use workspace::Workspace;
+use workspace::{Workspace, client_side_decorations};
use zed_actions::assistant::InlineAssist;
use prompt_store::*;
@@ -110,15 +111,22 @@ pub fn open_rules_library(
cx.update(|cx| {
let app_id = ReleaseChannel::global(cx).app_id();
let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
+ let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
+ Ok(val) if val == "server" => gpui::WindowDecorations::Server,
+ Ok(val) if val == "client" => gpui::WindowDecorations::Client,
+ _ => gpui::WindowDecorations::Client,
+ };
cx.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some("Rules Library".into()),
- appears_transparent: cfg!(target_os = "macos"),
+ appears_transparent: true,
traffic_light_position: Some(point(px(9.0), px(9.0))),
}),
app_id: Some(app_id.to_owned()),
window_bounds: Some(WindowBounds::Windowed(bounds)),
+ window_background: cx.theme().window_background_appearance(),
+ window_decorations: Some(window_decorations),
..Default::default()
},
|window, cx| {
@@ -140,6 +148,7 @@ pub fn open_rules_library(
}
pub struct RulesLibrary {
+ title_bar: Option<Entity<PlatformTitleBar>>,
store: Entity<PromptStore>,
language_registry: Arc<LanguageRegistry>,
rule_editors: HashMap<PromptId, RuleEditor>,
@@ -395,6 +404,11 @@ impl RulesLibrary {
picker
});
Self {
+ title_bar: if !cfg!(target_os = "macos") {
+ Some(cx.new(|_| PlatformTitleBar::new("rules-library-title-bar")))
+ } else {
+ None
+ },
store: store.clone(),
language_registry,
rule_editors: HashMap::default(),
@@ -1225,75 +1239,90 @@ impl Render for RulesLibrary {
let ui_font = theme::setup_ui_font(window, cx);
let theme = cx.theme().clone();
- h_flex()
- .id("rules-library")
- .key_context("PromptLibrary")
- .on_action(cx.listener(|this, &NewRule, window, cx| this.new_rule(window, cx)))
- .on_action(
- cx.listener(|this, &DeleteRule, window, cx| this.delete_active_rule(window, cx)),
- )
- .on_action(cx.listener(|this, &DuplicateRule, window, cx| {
- this.duplicate_active_rule(window, cx)
- }))
- .on_action(cx.listener(|this, &ToggleDefaultRule, window, cx| {
- this.toggle_default_for_active_rule(window, cx)
- }))
- .size_full()
- .overflow_hidden()
- .font(ui_font)
- .text_color(theme.colors().text)
- .child(self.render_rule_list(cx))
- .map(|el| {
- if self.store.read(cx).prompt_count() == 0 {
- el.child(
- v_flex()
- .w_2_3()
- .h_full()
- .items_center()
- .justify_center()
- .gap_4()
- .bg(cx.theme().colors().editor_background)
- .child(
- h_flex()
- .gap_2()
- .child(
- Icon::new(IconName::Book)
- .size(IconSize::Medium)
- .color(Color::Muted),
- )
- .child(
- Label::new("No rules yet")
- .size(LabelSize::Large)
- .color(Color::Muted),
- ),
- )
- .child(
- h_flex()
- .child(h_flex())
- .child(
- v_flex()
- .gap_1()
- .child(Label::new("Create your first rule:"))
- .child(
- Button::new("create-rule", "New Rule")
- .full_width()
- .key_binding(KeyBinding::for_action(
- &NewRule, window, cx,
- ))
- .on_click(|_, window, cx| {
- window.dispatch_action(
- NewRule.boxed_clone(),
- cx,
- )
- }),
- ),
- )
- .child(h_flex()),
- ),
- )
- } else {
- el.child(self.render_active_rule(cx))
- }
- })
+ client_side_decorations(
+ v_flex()
+ .id("rules-library")
+ .key_context("PromptLibrary")
+ .on_action(cx.listener(|this, &NewRule, window, cx| this.new_rule(window, cx)))
+ .on_action(
+ cx.listener(|this, &DeleteRule, window, cx| {
+ this.delete_active_rule(window, cx)
+ }),
+ )
+ .on_action(cx.listener(|this, &DuplicateRule, window, cx| {
+ this.duplicate_active_rule(window, cx)
+ }))
+ .on_action(cx.listener(|this, &ToggleDefaultRule, window, cx| {
+ this.toggle_default_for_active_rule(window, cx)
+ }))
+ .size_full()
+ .overflow_hidden()
+ .font(ui_font)
+ .text_color(theme.colors().text)
+ .children(self.title_bar.clone())
+ .child(
+ h_flex()
+ .flex_1()
+ .child(self.render_rule_list(cx))
+ .map(|el| {
+ if self.store.read(cx).prompt_count() == 0 {
+ el.child(
+ v_flex()
+ .w_2_3()
+ .h_full()
+ .items_center()
+ .justify_center()
+ .gap_4()
+ .bg(cx.theme().colors().editor_background)
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Icon::new(IconName::Book)
+ .size(IconSize::Medium)
+ .color(Color::Muted),
+ )
+ .child(
+ Label::new("No rules yet")
+ .size(LabelSize::Large)
+ .color(Color::Muted),
+ ),
+ )
+ .child(
+ h_flex()
+ .child(h_flex())
+ .child(
+ v_flex()
+ .gap_1()
+ .child(Label::new(
+ "Create your first rule:",
+ ))
+ .child(
+ Button::new("create-rule", "New Rule")
+ .full_width()
+ .key_binding(
+ KeyBinding::for_action(
+ &NewRule, window, cx,
+ ),
+ )
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ NewRule.boxed_clone(),
+ cx,
+ )
+ }),
+ ),
+ )
+ .child(h_flex()),
+ ),
+ )
+ } else {
+ el.child(self.render_active_rule(cx))
+ }
+ }),
+ ),
+ window,
+ cx,
+ )
}
}
@@ -0,0 +1,169 @@
+use gpui::{
+ AnyElement, Context, Decorations, InteractiveElement, IntoElement, MouseButton, ParentElement,
+ Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px,
+};
+use smallvec::SmallVec;
+use std::mem;
+use ui::prelude::*;
+
+use crate::platforms::{platform_linux, platform_mac, platform_windows};
+
+pub struct PlatformTitleBar {
+ id: ElementId,
+ platform_style: PlatformStyle,
+ children: SmallVec<[AnyElement; 2]>,
+ should_move: bool,
+}
+
+impl PlatformTitleBar {
+ pub fn new(id: impl Into<ElementId>) -> Self {
+ let platform_style = PlatformStyle::platform();
+ Self {
+ id: id.into(),
+ platform_style,
+ children: SmallVec::new(),
+ should_move: false,
+ }
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn height(window: &mut Window) -> Pixels {
+ (1.75 * window.rem_size()).max(px(34.))
+ }
+
+ #[cfg(target_os = "windows")]
+ pub fn height(_window: &mut Window) -> Pixels {
+ // todo(windows) instead of hard coded size report the actual size to the Windows platform API
+ px(32.)
+ }
+
+ pub fn set_children<T>(&mut self, children: T)
+ where
+ T: IntoIterator<Item = AnyElement>,
+ {
+ self.children = children.into_iter().collect();
+ }
+}
+
+impl Render for PlatformTitleBar {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let supported_controls = window.window_controls();
+ let decorations = window.window_decorations();
+ let height = Self::height(window);
+ let titlebar_color = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
+ if window.is_window_active() && !self.should_move {
+ cx.theme().colors().title_bar_background
+ } else {
+ cx.theme().colors().title_bar_inactive_background
+ }
+ } else {
+ cx.theme().colors().title_bar_background
+ };
+ let close_action = Box::new(workspace::CloseWindow);
+ let children = mem::take(&mut self.children);
+
+ h_flex()
+ .window_control_area(WindowControlArea::Drag)
+ .w_full()
+ .h(height)
+ .map(|this| {
+ if window.is_fullscreen() {
+ this.pl_2()
+ } else if self.platform_style == PlatformStyle::Mac {
+ this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
+ } else {
+ this.pl_2()
+ }
+ })
+ .map(|el| match decorations {
+ Decorations::Server => el,
+ Decorations::Client { tiling, .. } => el
+ .when(!(tiling.top || tiling.right), |el| {
+ el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+ })
+ .when(!(tiling.top || tiling.left), |el| {
+ el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
+ })
+ // this border is to avoid a transparent gap in the rounded corners
+ .mt(px(-1.))
+ .border(px(1.))
+ .border_color(titlebar_color),
+ })
+ .bg(titlebar_color)
+ .content_stretch()
+ .child(
+ div()
+ .id(self.id.clone())
+ .flex()
+ .flex_row()
+ .items_center()
+ .justify_between()
+ .w_full()
+ // Note: On Windows the title bar behavior is handled by the platform implementation.
+ .when(self.platform_style == PlatformStyle::Mac, |this| {
+ this.on_click(|event, window, _| {
+ if event.up.click_count == 2 {
+ window.titlebar_double_click();
+ }
+ })
+ })
+ .when(self.platform_style == PlatformStyle::Linux, |this| {
+ this.on_click(|event, window, _| {
+ if event.up.click_count == 2 {
+ window.zoom_window();
+ }
+ })
+ })
+ .children(children),
+ )
+ .when(!window.is_fullscreen(), |title_bar| {
+ match self.platform_style {
+ PlatformStyle::Mac => title_bar,
+ PlatformStyle::Linux => {
+ if matches!(decorations, Decorations::Client { .. }) {
+ title_bar
+ .child(platform_linux::LinuxWindowControls::new(close_action))
+ .when(supported_controls.window_menu, |titlebar| {
+ titlebar
+ .on_mouse_down(MouseButton::Right, move |ev, window, _| {
+ window.show_window_menu(ev.position)
+ })
+ })
+ .on_mouse_move(cx.listener(move |this, _ev, window, _| {
+ if this.should_move {
+ this.should_move = false;
+ window.start_window_move();
+ }
+ }))
+ .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
+ this.should_move = false;
+ }))
+ .on_mouse_up(
+ MouseButton::Left,
+ cx.listener(move |this, _ev, _window, _cx| {
+ this.should_move = false;
+ }),
+ )
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |this, _ev, _window, _cx| {
+ this.should_move = true;
+ }),
+ )
+ } else {
+ title_bar
+ }
+ }
+ PlatformStyle::Windows => {
+ title_bar.child(platform_windows::WindowsWindowControls::new(height))
+ }
+ }
+ })
+ }
+}
+
+impl ParentElement for PlatformTitleBar {
+ fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
+ self.children.extend(elements)
+ }
+}
@@ -1,34 +1,32 @@
mod application_menu;
mod collab;
mod onboarding_banner;
+pub mod platform_title_bar;
mod platforms;
mod title_bar_settings;
#[cfg(feature = "stories")]
mod stories;
-use crate::application_menu::ApplicationMenu;
+use crate::{application_menu::ApplicationMenu, platform_title_bar::PlatformTitleBar};
#[cfg(not(target_os = "macos"))]
use crate::application_menu::{
ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu,
};
-use crate::platforms::{platform_linux, platform_mac, platform_windows};
use auto_update::AutoUpdateStatus;
use call::ActiveCall;
use client::{Client, UserStore};
use gpui::{
- Action, AnyElement, App, Context, Corner, Decorations, Element, Entity, InteractiveElement,
- Interactivity, IntoElement, MouseButton, ParentElement, Render, Stateful,
- StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, WindowControlArea,
- actions, div, px,
+ Action, AnyElement, App, Context, Corner, Element, Entity, InteractiveElement, IntoElement,
+ MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, Subscription,
+ WeakEntity, Window, actions, div,
};
use onboarding_banner::OnboardingBanner;
use project::Project;
use rpc::proto;
use settings::Settings as _;
-use smallvec::SmallVec;
use std::sync::Arc;
use theme::ActiveTheme;
use title_bar_settings::TitleBarSettings;
@@ -111,14 +109,11 @@ pub fn init(cx: &mut App) {
}
pub struct TitleBar {
- platform_style: PlatformStyle,
- content: Stateful<Div>,
- children: SmallVec<[AnyElement; 2]>,
+ platform_titlebar: Entity<PlatformTitleBar>,
project: Entity<Project>,
user_store: Entity<UserStore>,
client: Arc<Client>,
workspace: WeakEntity<Workspace>,
- should_move: bool,
application_menu: Option<Entity<ApplicationMenu>>,
_subscriptions: Vec<Subscription>,
banner: Entity<OnboardingBanner>,
@@ -127,173 +122,69 @@ pub struct TitleBar {
impl Render for TitleBar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let title_bar_settings = *TitleBarSettings::get_global(cx);
- let close_action = Box::new(workspace::CloseWindow);
- let height = Self::height(window);
- let supported_controls = window.window_controls();
- let decorations = window.window_decorations();
- let titlebar_color = if cfg!(any(target_os = "linux", target_os = "freebsd")) {
- if window.is_window_active() && !self.should_move {
- cx.theme().colors().title_bar_background
- } else {
- cx.theme().colors().title_bar_inactive_background
- }
- } else {
- cx.theme().colors().title_bar_background
- };
- h_flex()
- .id("titlebar")
- .window_control_area(WindowControlArea::Drag)
- .w_full()
- .h(height)
- .map(|this| {
- if window.is_fullscreen() {
- this.pl_2()
- } else if self.platform_style == PlatformStyle::Mac {
- this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING))
- } else {
- this.pl_2()
- }
- })
- .map(|el| match decorations {
- Decorations::Server => el,
- Decorations::Client { tiling, .. } => el
- .when(!(tiling.top || tiling.right), |el| {
- el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
- })
- .when(!(tiling.top || tiling.left), |el| {
- el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
- })
- // this border is to avoid a transparent gap in the rounded corners
- .mt(px(-1.))
- .border(px(1.))
- .border_color(titlebar_color),
- })
- .bg(titlebar_color)
- .content_stretch()
- .child(
- div()
- .id("titlebar-content")
- .flex()
- .flex_row()
- .items_center()
- .justify_between()
- .w_full()
- // Note: On Windows the title bar behavior is handled by the platform implementation.
- .when(self.platform_style == PlatformStyle::Mac, |this| {
- this.on_click(|event, window, _| {
- if event.up.click_count == 2 {
- window.titlebar_double_click();
- }
+ let mut children = Vec::new();
+
+ children.push(
+ h_flex()
+ .gap_1()
+ .map(|title_bar| {
+ let mut render_project_items = title_bar_settings.show_branch_name
+ || title_bar_settings.show_project_items;
+ title_bar
+ .when_some(self.application_menu.clone(), |title_bar, menu| {
+ render_project_items &= !menu.read(cx).all_menus_shown();
+ title_bar.child(menu)
})
- })
- .when(self.platform_style == PlatformStyle::Linux, |this| {
- this.on_click(|event, window, _| {
- if event.up.click_count == 2 {
- window.zoom_window();
- }
- })
- })
- .child(
- h_flex()
- .gap_1()
- .map(|title_bar| {
- let mut render_project_items = title_bar_settings.show_branch_name
- || title_bar_settings.show_project_items;
- title_bar
- .when_some(self.application_menu.clone(), |title_bar, menu| {
- render_project_items &= !menu.read(cx).all_menus_shown();
- title_bar.child(menu)
- })
- .when(render_project_items, |title_bar| {
- title_bar
- .when(
- title_bar_settings.show_project_items,
- |title_bar| {
- title_bar
- .children(self.render_project_host(cx))
- .child(self.render_project_name(cx))
- },
- )
- .when(
- title_bar_settings.show_branch_name,
- |title_bar| {
- title_bar
- .children(self.render_project_branch(cx))
- },
- )
- })
- })
- .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()),
- )
- .child(self.render_collaborator_list(window, cx))
- .when(title_bar_settings.show_onboarding_banner, |title_bar| {
- title_bar.child(self.banner.clone())
- })
- .child(
- h_flex()
- .gap_1()
- .pr_1()
- .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
- .children(self.render_call_controls(window, cx))
- .map(|el| {
- let status = self.client.status();
- let status = &*status.borrow();
- if matches!(status, client::Status::Connected { .. }) {
- el.child(self.render_user_menu_button(cx))
- } else {
- el.children(self.render_connection_status(status, cx))
- .when(TitleBarSettings::get_global(cx).show_sign_in, |el| {
- el.child(self.render_sign_in_button(cx))
- })
- .child(self.render_user_menu_button(cx))
- }
- }),
- ),
- )
- .when(!window.is_fullscreen(), |title_bar| {
- match self.platform_style {
- PlatformStyle::Mac => title_bar,
- PlatformStyle::Linux => {
- if matches!(decorations, Decorations::Client { .. }) {
+ .when(render_project_items, |title_bar| {
title_bar
- .child(platform_linux::LinuxWindowControls::new(close_action))
- .when(supported_controls.window_menu, |titlebar| {
- titlebar.on_mouse_down(
- gpui::MouseButton::Right,
- move |ev, window, _| window.show_window_menu(ev.position),
- )
+ .when(title_bar_settings.show_project_items, |title_bar| {
+ title_bar
+ .children(self.render_project_host(cx))
+ .child(self.render_project_name(cx))
})
- .on_mouse_move(cx.listener(move |this, _ev, window, _| {
- if this.should_move {
- this.should_move = false;
- window.start_window_move();
- }
- }))
- .on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| {
- this.should_move = false;
- }))
- .on_mouse_up(
- gpui::MouseButton::Left,
- cx.listener(move |this, _ev, _window, _cx| {
- this.should_move = false;
- }),
- )
- .on_mouse_down(
- gpui::MouseButton::Left,
- cx.listener(move |this, _ev, _window, _cx| {
- this.should_move = true;
- }),
- )
- } else {
- title_bar
- }
- }
- PlatformStyle::Windows => {
- title_bar.child(platform_windows::WindowsWindowControls::new(height))
+ .when(title_bar_settings.show_branch_name, |title_bar| {
+ title_bar.children(self.render_project_branch(cx))
+ })
+ })
+ })
+ .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
+ .into_any_element(),
+ );
+
+ children.push(self.render_collaborator_list(window, cx).into_any_element());
+
+ if title_bar_settings.show_onboarding_banner {
+ children.push(self.banner.clone().into_any_element())
+ }
+
+ children.push(
+ h_flex()
+ .gap_1()
+ .pr_1()
+ .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
+ .children(self.render_call_controls(window, cx))
+ .map(|el| {
+ let status = self.client.status();
+ let status = &*status.borrow();
+ if matches!(status, client::Status::Connected { .. }) {
+ el.child(self.render_user_menu_button(cx))
+ } else {
+ el.children(self.render_connection_status(status, cx))
+ .when(TitleBarSettings::get_global(cx).show_sign_in, |el| {
+ el.child(self.render_sign_in_button(cx))
+ })
+ .child(self.render_user_menu_button(cx))
}
- }
- })
+ })
+ .into_any_element(),
+ );
+
+ self.platform_titlebar.update(cx, |this, _| {
+ this.set_children(children);
+ });
+
+ self.platform_titlebar.clone().into_any_element()
}
}
@@ -345,13 +236,12 @@ impl TitleBar {
)
});
+ let platform_titlebar = cx.new(|_| PlatformTitleBar::new(id));
+
Self {
- platform_style,
- content: div().id(id.into()),
- children: SmallVec::new(),
+ platform_titlebar,
application_menu,
workspace: workspace.weak_handle(),
- should_move: false,
project,
user_store,
client,
@@ -360,23 +250,6 @@ impl TitleBar {
}
}
- #[cfg(not(target_os = "windows"))]
- pub fn height(window: &mut Window) -> Pixels {
- (1.75 * window.rem_size()).max(px(34.))
- }
-
- #[cfg(target_os = "windows")]
- pub fn height(_window: &mut Window) -> Pixels {
- // todo(windows) instead of hard coded size report the actual size to the Windows platform API
- px(32.)
- }
-
- /// Sets the platform style.
- pub fn platform_style(mut self, style: PlatformStyle) -> Self {
- self.platform_style = style;
- self
- }
-
fn render_ssh_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let options = self.project.read(cx).ssh_connection_options(cx)?;
let host: SharedString = options.connection_string().into();
@@ -796,17 +669,3 @@ impl TitleBar {
}
}
}
-
-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)
- }
-}