diff --git a/Cargo.lock b/Cargo.lock index a0d4c5aff0..20f6fd2994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13607,6 +13607,7 @@ dependencies = [ "serde", "settings", "theme", + "title_bar", "ui", "util", "workspace", diff --git a/crates/rules_library/Cargo.toml b/crates/rules_library/Cargo.toml index 48048d191a..298f77a2d2 100644 --- a/crates/rules_library/Cargo.toml +++ b/crates/rules_library/Cargo.toml @@ -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 diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 7f145f3139..231647ef5a 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -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>, store: Entity, language_registry: Arc, rule_editors: HashMap, @@ -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, + ) } } diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs new file mode 100644 index 0000000000..5331ed39dd --- /dev/null +++ b/crates/title_bar/src/platform_title_bar.rs @@ -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) -> 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(&mut self, children: T) + where + T: IntoIterator, + { + self.children = children.into_iter().collect(); + } +} + +impl Render for PlatformTitleBar { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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) { + self.children.extend(elements) + } +} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 418bb70f8d..53d1397226 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -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
, - children: SmallVec<[AnyElement; 2]>, + platform_titlebar: Entity, project: Entity, user_store: Entity, client: Arc, workspace: WeakEntity, - should_move: bool, application_menu: Option>, _subscriptions: Vec, banner: Entity, @@ -127,173 +122,69 @@ pub struct TitleBar { impl Render for TitleBar { fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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 - } + .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)) } - PlatformStyle::Windows => { - title_bar.child(platform_windows::WindowsWindowControls::new(height)) - } - } - }) + }) + .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) -> Option { 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) { - self.children.extend(elements) - } -}