diff --git a/Cargo.lock b/Cargo.lock index 1cac85e0c7..f682d76ad5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9002,7 +9002,6 @@ dependencies = [ "gpui", "language", "log", - "markdown", "menu", "ordered-float 2.10.1", "picker", diff --git a/assets/icons/trash_alt.svg b/assets/icons/trash_alt.svg new file mode 100644 index 0000000000..6867b42147 --- /dev/null +++ b/assets/icons/trash_alt.svg @@ -0,0 +1 @@ + diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index da4ee210e1..2eea6321a0 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -22,7 +22,6 @@ futures.workspace = true fuzzy.workspace = true gpui.workspace = true log.workspace = true -markdown.workspace = true menu.workspace = true ordered-float.workspace = true picker.workspace = true diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 722743e0ff..7761461ab5 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -8,17 +8,18 @@ use anyhow::Result; use client::Client; use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId}; use editor::Editor; +use gpui::pulsating_between; use gpui::AsyncWindowContext; +use gpui::ClipboardItem; use gpui::PathPromptOptions; use gpui::Subscription; use gpui::Task; use gpui::WeakView; use gpui::{ - percentage, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter, - FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext, + percentage, Action, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, + EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, + ViewContext, }; -use markdown::Markdown; -use markdown::MarkdownStyle; use project::terminals::wrap_for_ssh; use project::terminals::SshCommand; use rpc::proto::RegenerateDevServerTokenResponse; @@ -35,8 +36,8 @@ use terminal_view::terminal_panel::TerminalPanel; use ui::ElevationIndex; use ui::Section; use ui::{ - prelude::*, Indicator, List, ListHeader, ListItem, Modal, ModalFooter, ModalHeader, - RadioWithLabel, Tooltip, + prelude::*, IconButtonShape, Indicator, List, ListItem, Modal, ModalFooter, ModalHeader, + Tooltip, }; use ui_input::{FieldLabelLayout, TextField}; use util::ResultExt; @@ -62,7 +63,6 @@ pub struct DevServerProjects { workspace: WeakView, project_path_input: View, dev_server_name_input: View, - markdown: View, _dev_server_subscription: Subscription, } @@ -132,26 +132,6 @@ impl DevServerProjects { ..Default::default() }); - let markdown_style = MarkdownStyle { - base_text_style: base_style, - code_block: gpui::StyleRefinement { - text: Some(gpui::TextStyleRefinement { - font_family: Some("Zed Plex Mono".into()), - ..Default::default() - }), - ..Default::default() - }, - link: gpui::TextStyleRefinement { - color: Some(Color::Accent.color(cx)), - ..Default::default() - }, - syntax: cx.theme().syntax().clone(), - selection_background_color: cx.theme().players().local().selection, - ..Default::default() - }; - let markdown = - cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None)); - Self { mode: Mode::Default(None), focus_handle, @@ -159,7 +139,6 @@ impl DevServerProjects { dev_server_store, project_path_input, dev_server_name_input, - markdown, workspace, _dev_server_subscription: subscription, } @@ -845,7 +824,7 @@ impl DevServerProjects { }) .child({ let dev_server_id = dev_server.id; - IconButton::new("remove-dev-server", IconName::Trash) + IconButton::new("remove-dev-server", IconName::TrashAlt) .on_click(cx.listener(move |this, _, cx| { this.delete_dev_server(dev_server_id, cx) })) @@ -913,40 +892,73 @@ impl DevServerProjects { ) -> impl IntoElement { v_flex() .w_full() + .px(Spacing::Small.rems(cx) + Spacing::Small.rems(cx)) .child( - h_flex().group("ssh-server").justify_between().child( - h_flex() - .gap_2() - .child( - div() - .id(("status", ix)) - .relative() - .child(Icon::new(IconName::Server).size(IconSize::Small)), - ) - .child( - div() - .max_w(rems(26.)) - .overflow_hidden() - .whitespace_nowrap() - .child(Label::new(ssh_connection.host.clone())), - ) - .child(h_flex().visible_on_hover("ssh-server").gap_1().child({ - IconButton::new("remove-dev-server", IconName::Trash) - .on_click( - cx.listener(move |this, _, cx| this.delete_ssh_server(ix, cx)), - ) - .tooltip(|cx| Tooltip::text("Remove Dev Server", cx)) - })), - ), + h_flex() + .w_full() + .group("ssh-server") + .justify_between() + .child( + h_flex() + .gap_2() + .w_full() + .child( + div() + .id(("status", ix)) + .relative() + .child(Icon::new(IconName::Server).size(IconSize::Small)), + ) + .child( + h_flex() + .max_w(rems(26.)) + .overflow_hidden() + .whitespace_nowrap() + .child(Label::new(ssh_connection.host.clone())), + ), + ) + .child( + h_flex() + .visible_on_hover("ssh-server") + .gap_1() + .child({ + IconButton::new("copy-dev-server-address", IconName::Copy) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |this, _, cx| { + this.update_settings_file(cx, move |servers, cx| { + if let Some(content) = servers + .ssh_connections + .as_ref() + .and_then(|connections| { + connections + .get(ix) + .map(|connection| connection.host.clone()) + }) + { + cx.write_to_clipboard(ClipboardItem::new_string( + content, + )); + } + }); + })) + .tooltip(|cx| Tooltip::text("Copy Server Address", cx)) + }) + .child({ + IconButton::new("remove-dev-server", IconName::TrashAlt) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |this, _, cx| { + this.delete_ssh_server(ix, cx) + })) + .tooltip(|cx| Tooltip::text("Remove Dev Server", cx)) + }), + ), ) .child( v_flex() .w_full() - .bg(cx.theme().colors().background) - .border_1() + .border_l_1() .border_color(cx.theme().colors().border_variant) - .rounded_md() .my_1() + .mx_1p5() .py_0p5() .px_3() .child( @@ -956,12 +968,17 @@ impl DevServerProjects { self.render_ssh_project(ix, &ssh_connection, pix, p, cx) })) .child( - ListItem::new("new-remote_project") - .start_slot(Icon::new(IconName::Plus)) - .child(Label::new("Open folder…")) - .on_click(cx.listener(move |this, _, cx| { - this.create_ssh_project(ix, ssh_connection.clone(), cx); - })), + h_flex().child( + Button::new("new-remote_project", "Open Folder…") + .icon(IconName::Plus) + .size(ButtonSize::Default) + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .icon_position(IconPosition::Start) + .on_click(cx.listener(move |this, _, cx| { + this.create_ssh_project(ix, ssh_connection.clone(), cx); + })), + ), ), ), ) @@ -978,7 +995,8 @@ impl DevServerProjects { let project = project.clone(); let server = server.clone(); ListItem::new(("remote-project", ix)) - .start_slot(Icon::new(IconName::FileTree)) + .spacing(ui::ListItemSpacing::Sparse) + .start_slot(Icon::new(IconName::Folder).color(Color::Muted)) .child(Label::new(project.paths.join(", "))) .on_click(cx.listener(move |this, _, cx| { let Some(app_state) = this @@ -1014,7 +1032,7 @@ impl DevServerProjects { .detach(); })) .end_hover_slot::(Some( - IconButton::new("remove-remote-project", IconName::Trash) + IconButton::new("remove-remote-project", IconName::TrashAlt) .on_click( cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)), ) @@ -1026,7 +1044,7 @@ impl DevServerProjects { fn update_settings_file( &mut self, cx: &mut ViewContext, - f: impl FnOnce(&mut RemoteSettingsContent) + Send + Sync + 'static, + f: impl FnOnce(&mut RemoteSettingsContent, &AppContext) + Send + Sync + 'static, ) { let Some(fs) = self .workspace @@ -1035,11 +1053,11 @@ impl DevServerProjects { else { return; }; - update_settings_file::(fs, cx, move |setting, _| f(setting)); + update_settings_file::(fs, cx, move |setting, cx| f(setting, cx)); } fn delete_ssh_server(&mut self, server: usize, cx: &mut ViewContext) { - self.update_settings_file(cx, move |setting| { + self.update_settings_file(cx, move |setting, _| { if let Some(connections) = setting.ssh_connections.as_mut() { connections.remove(server); } @@ -1047,7 +1065,7 @@ impl DevServerProjects { } fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext) { - self.update_settings_file(cx, move |setting| { + self.update_settings_file(cx, move |setting, _| { if let Some(server) = setting .ssh_connections .as_mut() @@ -1063,7 +1081,7 @@ impl DevServerProjects { connection_options: remote::SshConnectionOptions, cx: &mut ViewContext, ) { - self.update_settings_file(cx, move |setting| { + self.update_settings_file(cx, move |setting, _| { setting .ssh_connections .get_or_insert(Default::default()) @@ -1124,7 +1142,7 @@ impl DevServerProjects { }).detach(); } })) - .end_hover_slot::(Some(IconButton::new("remove-remote-project", IconName::Trash) + .end_hover_slot::(Some(IconButton::new("remove-remote-project", IconName::TrashAlt) .on_click(cx.listener(move |this, _, cx| { this.delete_dev_server_project(dev_server_project_id, cx) })) @@ -1148,250 +1166,109 @@ impl DevServerProjects { kind = NewServerKind::DirectSSH; } - let status = dev_server_id - .map(|id| self.dev_server_store.read(cx).dev_server_status(id)) - .unwrap_or_default(); - - let name = self.dev_server_name_input.update(cx, |input, cx| { + self.dev_server_name_input.update(cx, |input, cx| { input.editor().update(cx, |editor, cx| { if editor.text(cx).is_empty() { - match kind { - NewServerKind::DirectSSH => editor.set_placeholder_text("ssh host", cx), - NewServerKind::LegacySSH => editor.set_placeholder_text("ssh host", cx), - NewServerKind::Manual => editor.set_placeholder_text("example-host", cx), - } + editor.set_placeholder_text("ssh me@my.server / ssh@secret-box:2222", cx); } - editor.text(cx) }) }); - - const MANUAL_SETUP_MESSAGE: &str = - "Generate a token for this server and follow the steps to set Zed up on that machine."; - const SSH_SETUP_MESSAGE: &str = - "Enter the command you use to SSH into this server.\nFor example: `ssh me@my.server` or `ssh me@secret-box:2222`."; - - Modal::new("create-dev-server", Some(self.scroll_handle.clone())) - .header( - ModalHeader::new() - .headline("Create Dev Server") - .show_back_button(true), - ) - .section( - Section::new() - .header(if kind == NewServerKind::Manual { - "Server Name".into() - } else { - "SSH arguments".into() - }) - .child( - div() - .max_w(rems(16.)) - .child(self.dev_server_name_input.clone()), - ), - ) - .section( - Section::new_contained() - .header("Connection Method".into()) - .child( - v_flex() - .w_full() - .px_2() - .gap_y(Spacing::Large.rems(cx)) - .when(ssh_prompt.is_none(), |el| { - el.child( - v_flex() - .when(use_direct_ssh, |el| { - el.child(RadioWithLabel::new( - "use-server-name-in-ssh", - Label::new("Connect via SSH (default)"), - NewServerKind::DirectSSH == kind, - cx.listener({ - move |this, _, cx| { - if let Mode::CreateDevServer( - CreateDevServer { kind, .. }, - ) = &mut this.mode - { - *kind = NewServerKind::DirectSSH; - } - cx.notify() - } - }), - )) - }) - .when(!use_direct_ssh, |el| { - el.child(RadioWithLabel::new( - "use-server-name-in-ssh", - Label::new("Configure over SSH (default)"), - kind == NewServerKind::LegacySSH, - cx.listener({ - move |this, _, cx| { - if let Mode::CreateDevServer( - CreateDevServer { kind, .. }, - ) = &mut this.mode - { - *kind = NewServerKind::LegacySSH; - } - cx.notify() - } - }), - )) - }) - .child(RadioWithLabel::new( - "use-server-name-in-ssh", - Label::new("Configure manually"), - kind == NewServerKind::Manual, - cx.listener({ - move |this, _, cx| { - if let Mode::CreateDevServer( - CreateDevServer { kind, .. }, - ) = &mut this.mode - { - *kind = NewServerKind::Manual; - } - cx.notify() - } - }), - )), - ) - }) - .when(dev_server_id.is_none() && ssh_prompt.is_none(), |el| { - el.child( - if kind == NewServerKind::Manual { - Label::new(MANUAL_SETUP_MESSAGE) - } else { - Label::new(SSH_SETUP_MESSAGE) - } - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .when_some(ssh_prompt, |el, ssh_prompt| el.child(ssh_prompt)) - .when(dev_server_id.is_some() && access_token.is_none(), |el| { - el.child( - if kind == NewServerKind::Manual { - Label::new( - "Note: updating the dev server generate a new token", - ) - } else { - Label::new(SSH_SETUP_MESSAGE) - } - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .when_some(access_token.clone(), { - |el, access_token| { - el.child(self.render_dev_server_token_creating( - access_token, - name, - kind, - status, - creating, - cx, - )) - } - }), - ), - ) - .footer( - ModalFooter::new().end_slot(if status == DevServerStatus::Online { - Button::new("create-dev-server", "Done") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .on_click(cx.listener(move |this, _, cx| { - cx.focus(&this.focus_handle); - this.mode = Mode::Default(None); - cx.notify(); - })) - } else { - Button::new( - "create-dev-server", - if kind == NewServerKind::Manual { - if dev_server_id.is_some() { - "Update" - } else { - "Create" - } - } else if dev_server_id.is_some() { - "Reconnect" - } else { - "Connect" - }, - ) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .disabled(creating && dev_server_id.is_none()) - .on_click(cx.listener({ - let access_token = access_token.clone(); - move |this, _, cx| { - if kind == NewServerKind::DirectSSH { - this.create_ssh_server(cx); - return; - } - this.create_or_update_dev_server( - kind, - dev_server_id, - access_token.clone(), - cx, - ); - } - })) - }), - ) - } - - fn render_dev_server_token_creating( - &self, - access_token: String, - dev_server_name: String, - kind: NewServerKind, - status: DevServerStatus, - creating: bool, - cx: &mut ViewContext, - ) -> Div { - self.markdown.update(cx, |markdown, cx| { - if kind == NewServerKind::Manual { - markdown.reset(format!("Please log into '{}'. If you don't yet have Zed installed, run:\n```\ncurl https://zed.dev/install.sh | bash\n```\nThen, to start Zed in headless mode:\n```\nzed --dev-server-token {}\n```", dev_server_name, access_token), cx); - } else { - markdown.reset("Please wait while we connect over SSH.\n\nIf you run into problems, please [file a bug](https://github.com/zed-industries/zed), and in the meantime try using the manual setup.".to_string(), cx); - } - }); - + let theme = cx.theme(); v_flex() - .pl_2() - .pt_2() - .gap_2() - .child(v_flex().w_full().text_sm().child(self.markdown.clone())) - .map(|el| { - if status == DevServerStatus::Offline && kind != NewServerKind::Manual && !creating - { - el.child( - h_flex() - .gap_2() - .child(Icon::new(IconName::Disconnected).size(IconSize::Medium)) - .child(Label::new("Not connected")), - ) - } else if status == DevServerStatus::Offline { - el.child(Self::render_loading_spinner("Waiting for connection…")) - } else { - el.child(Label::new("🎊 Connection established!")) - } - }) - } - - fn render_loading_spinner(label: impl Into) -> Div { - h_flex() - .gap_2() + .id("create-dev-server") + .overflow_hidden() + .size_full() + .flex_1() .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Medium) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + h_flex() + .p_2() + .gap_2() + .items_center() + .border_b_1() + .border_color(theme.colors().border_variant) + .child( + IconButton::new("cancel-dev-server-creation", IconName::ArrowLeft) + .shape(IconButtonShape::Square) + .on_click(|_, cx| { + cx.dispatch_action(menu::Cancel.boxed_clone()); + }), + ) + .child(Label::new("Connect New Dev Server")), + ) + .child( + v_flex() + .p_3() + .border_b_1() + .border_color(theme.colors().border_variant) + .child(Label::new("SSH Arguments")) + .child( + Label::new("Enter the command you use to SSH into this server.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .mt_2() + .w_full() + .gap_2() + .child(self.dev_server_name_input.clone()) + .child( + Button::new("create-dev-server", "Connect Server") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .disabled(creating && dev_server_id.is_none()) + .on_click(cx.listener({ + let access_token = access_token.clone(); + move |this, _, cx| { + if kind == NewServerKind::DirectSSH { + this.create_ssh_server(cx); + return; + } + this.create_or_update_dev_server( + kind, + dev_server_id, + access_token.clone(), + cx, + ); + } + })), + ), ), ) - .child(Label::new(label)) + .child( + h_flex() + .bg(theme.colors().editor_background) + .w_full() + .map(|this| { + if let Some(ssh_prompt) = ssh_prompt { + this.child(h_flex().w_full().child(ssh_prompt)) + } else { + let color = Color::Muted.color(cx); + this.child( + h_flex() + .p_2() + .w_full() + .content_center() + .gap_2() + .child(h_flex().w_full()) + .child( + div().p_1().rounded_lg().bg(color).with_animation( + "pulse-ssh-waiting-for-connection", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.2, 0.5)), + move |this, progress| this.bg(color.opacity(progress)), + ), + ) + .child( + Label::new("Waiting for connection…") + .size(LabelSize::Small), + ) + .child(h_flex().w_full()), + ) + } + }), + ) } fn render_default(&mut self, cx: &mut ViewContext) -> impl IntoElement { @@ -1416,64 +1293,73 @@ impl DevServerProjects { creating_dev_server = Some(*dev_server_id); }; + let footer = format!("Connections: {}", ssh_connections.len() + dev_servers.len()); Modal::new("remote-projects", Some(self.scroll_handle.clone())) .header( - ModalHeader::new() - .show_dismiss_button(true) - .child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::Small)), + ModalHeader::new().child( + h_flex() + .justify_between() + .child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall)) + .child( + Button::new("register-dev-server-button", "Connect New Server") + .style(ButtonStyle::Filled) + .layer(ElevationIndex::ModalSurface) + .icon(IconName::Plus) + .icon_position(IconPosition::Start) + .icon_color(Color::Muted) + .on_click(cx.listener(|this, _, cx| { + this.mode = Mode::CreateDevServer(CreateDevServer { + kind: if SshSettings::get_global(cx).use_direct_ssh() { + NewServerKind::DirectSSH + } else { + NewServerKind::LegacySSH + }, + ..Default::default() + }); + this.dev_server_name_input.update(cx, |text_field, cx| { + text_field.editor().update(cx, |editor, cx| { + editor.set_text("", cx); + }); + }); + cx.notify(); + })), + ), + ), ) .section( - Section::new().child( - div().child( - List::new() - .empty_message("No dev servers registered yet.") - .header(Some( - ListHeader::new("Connections").end_slot( - Button::new("register-dev-server-button", "Connect New Server") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_color(Color::Muted) - .on_click(cx.listener(|this, _, cx| { - this.mode = Mode::CreateDevServer(CreateDevServer { - kind: if SshSettings::get_global(cx) - .use_direct_ssh() - { - NewServerKind::DirectSSH - } else { - NewServerKind::LegacySSH - }, - ..Default::default() - }); - this.dev_server_name_input.update( - cx, - |text_field, cx| { - text_field.editor().update(cx, |editor, cx| { - editor.set_text("", cx); - }); - }, - ); - cx.notify(); - })), - ), - )) - .children(ssh_connections.iter().cloned().enumerate().map( - |(ix, connection)| { - self.render_ssh_connection(ix, connection, cx) - .into_any_element() - }, - )) - .children(dev_servers.iter().map(|dev_server| { - let creating = if creating_dev_server == Some(dev_server.id) { - is_creating - } else { - None - }; - self.render_dev_server(dev_server, creating, cx) - .into_any_element() - })), - ), + Section::new().padded(false).child( + div() + .border_y_1() + .border_color(cx.theme().colors().border_variant) + .w_full() + .child( + div().p_2().child( + List::new() + .empty_message("No dev servers registered yet.") + .children(ssh_connections.iter().cloned().enumerate().map( + |(ix, connection)| { + self.render_ssh_connection(ix, connection, cx) + .into_any_element() + }, + )) + .children(dev_servers.iter().map(|dev_server| { + let creating = if creating_dev_server == Some(dev_server.id) + { + is_creating + } else { + None + }; + self.render_dev_server(dev_server, creating, cx) + .into_any_element() + })), + ), + ), ), ) + .footer( + ModalFooter::new() + .start_slot(div().child(Label::new(footer).size(LabelSize::Small))), + ) } } @@ -1501,7 +1387,6 @@ impl Render for DevServerProjects { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { div() .track_focus(&self.focus_handle) - .p_2() .elevation_3(cx) .key_context("DevServerModal") .on_action(cx.listener(Self::cancel)) diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 9e50523773..554146eab2 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -5,9 +5,9 @@ use auto_update::AutoUpdater; use editor::Editor; use futures::channel::oneshot; use gpui::{ - percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent, - EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task, - Transformation, View, + percentage, px, Action, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, + DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, + SharedString, Task, Transformation, View, }; use gpui::{AppContext, Model}; use release_channel::{AppVersion, ReleaseChannel}; @@ -16,9 +16,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources}; use ui::{ - h_flex, v_flex, Color, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, - IntoElement, Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext, - WindowContext, + div, h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, FluentBuilder as _, Icon, + IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, Styled, + StyledExt as _, Tooltip, ViewContext, VisualContext, WindowContext, }; use workspace::{AppState, ModalView, Workspace}; @@ -140,47 +140,57 @@ impl SshPrompt { } impl Render for SshPrompt { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { v_flex() + .w_full() .key_context("PasswordPrompt") - .p_4() - .size_full() + .justify_start() .child( - h_flex() - .gap_2() - .child(if self.error_message.is_some() { - Icon::new(IconName::XCircle) - .size(IconSize::Medium) - .color(Color::Error) - .into_any_element() - } else { - Icon::new(IconName::ArrowCircle) - .size(IconSize::Medium) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ) - .into_any_element() - }) + v_flex() + .p_4() + .size_full() .child( - Label::new(format!("ssh {}…", self.connection_string)) - .size(ui::LabelSize::Large), - ), + h_flex() + .gap_2() + .justify_between() + .child(h_flex().w_full()) + .child(if self.error_message.is_some() { + Icon::new(IconName::XCircle) + .size(IconSize::Medium) + .color(Color::Error) + .into_any_element() + } else { + Icon::new(IconName::ArrowCircle) + .size(IconSize::Medium) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage( + delta, + ))) + }, + ) + .into_any_element() + }) + .child(Label::new(format!( + "Connecting to {}…", + self.connection_string + ))) + .child(h_flex().w_full()), + ) + .when_some(self.error_message.as_ref(), |el, error| { + el.child(Label::new(error.clone())) + }) + .when( + self.error_message.is_none() && self.status_message.is_some(), + |el| el.child(Label::new(self.status_message.clone().unwrap())), + ) + .when_some(self.prompt.as_ref(), |el, prompt| { + el.child(Label::new(prompt.0.clone())) + .child(self.editor.clone()) + }), ) - .when_some(self.error_message.as_ref(), |el, error| { - el.child(Label::new(error.clone())) - }) - .when( - self.error_message.is_none() && self.status_message.is_some(), - |el| el.child(Label::new(self.status_message.clone().unwrap())), - ) - .when_some(self.prompt.as_ref(), |el, prompt| { - el.child(Label::new(prompt.0.clone())) - .child(self.editor.clone()) - }) } } @@ -202,14 +212,41 @@ impl SshConnectionModal { impl Render for SshConnectionModal { fn render(&mut self, cx: &mut ui::ViewContext) -> impl ui::IntoElement { + let connection_string = self.prompt.read(cx).connection_string.clone(); + let theme = cx.theme(); + let header_color = theme.colors().element_background; + let body_color = theme.colors().background; v_flex() .elevation_3(cx) - .p_4() - .gap_2() .on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::confirm)) .w(px(400.)) - .child(self.prompt.clone()) + .child( + h_flex() + .p_1() + .border_b_1() + .border_color(theme.colors().border) + .bg(header_color) + .justify_between() + .child( + IconButton::new("ssh-connection-cancel", IconName::ArrowLeft) + .icon_size(IconSize::XSmall) + .on_click(|_, cx| cx.dispatch_action(menu::Cancel.boxed_clone())) + .tooltip(|cx| Tooltip::for_action("Back", &menu::Cancel, cx)), + ) + .child( + h_flex() + .gap_2() + .child(Icon::new(IconName::Server).size(IconSize::XSmall)) + .child( + Label::new(connection_string) + .size(ui::LabelSize::Small) + .single_line(), + ), + ) + .child(div()), + ) + .child(h_flex().bg(body_color).w_full().child(self.prompt.clone())) } } diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 693caaaafd..8d374ef67c 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -275,6 +275,7 @@ pub enum IconName { Tab, Terminal, Trash, + TrashAlt, TriangleRight, Undo, Unpin, diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs index 11611f9c0f..512f9601a8 100644 --- a/crates/ui/src/components/modal.rs +++ b/crates/ui/src/components/modal.rs @@ -262,6 +262,7 @@ impl RenderOnce for ModalFooter { #[derive(IntoElement)] pub struct Section { contained: bool, + padded: bool, header: Option, meta: Option, children: SmallVec<[AnyElement; 2]>, @@ -277,6 +278,7 @@ impl Section { pub fn new() -> Self { Self { contained: false, + padded: true, header: None, meta: None, children: SmallVec::new(), @@ -286,6 +288,7 @@ impl Section { pub fn new_contained() -> Self { Self { contained: true, + padded: true, header: None, meta: None, children: SmallVec::new(), @@ -306,6 +309,10 @@ impl Section { self.meta = Some(meta.into()); self } + pub fn padded(mut self, padded: bool) -> Self { + self.padded = padded; + self + } } impl ParentElement for Section { @@ -320,22 +327,27 @@ impl RenderOnce for Section { section_bg.fade_out(0.96); let children = if self.contained { - v_flex().flex_1().px(Spacing::XLarge.rems(cx)).child( - v_flex() - .w_full() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(section_bg) - .py(Spacing::Medium.rems(cx)) - .gap_y(Spacing::Small.rems(cx)) - .child(div().flex().flex_1().size_full().children(self.children)), - ) + v_flex() + .flex_1() + .when(self.padded, |this| this.px(Spacing::XLarge.rems(cx))) + .child( + v_flex() + .w_full() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(section_bg) + .py(Spacing::Medium.rems(cx)) + .gap_y(Spacing::Small.rems(cx)) + .child(div().flex().flex_1().size_full().children(self.children)), + ) } else { v_flex() .w_full() .gap_y(Spacing::Small.rems(cx)) - .px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx)) + .when(self.padded, |this| { + this.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx)) + }) .children(self.children) }; diff --git a/crates/ui/src/traits/styled_ext.rs b/crates/ui/src/traits/styled_ext.rs index 997e80ca86..09d8a4f74f 100644 --- a/crates/ui/src/traits/styled_ext.rs +++ b/crates/ui/src/traits/styled_ext.rs @@ -3,7 +3,7 @@ use gpui::{hsla, Styled, WindowContext}; use crate::prelude::*; use crate::ElevationIndex; -fn elevated(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E { +fn elevated(this: E, cx: &WindowContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) .rounded_lg() .border_1() @@ -11,7 +11,7 @@ fn elevated(this: E, cx: &mut WindowContext, index: ElevationIndex) - .shadow(index.shadow()) } -fn elevated_borderless(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E { +fn elevated_borderless(this: E, cx: &WindowContext, index: ElevationIndex) -> E { this.bg(cx.theme().colors().elevated_surface_background) .rounded_lg() .shadow(index.shadow()) @@ -38,14 +38,14 @@ pub trait StyledExt: Styled + Sized { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Example Elements: Title Bar, Panel, Tab Bar, Editor - fn elevation_1(self, cx: &mut WindowContext) -> Self { + fn elevation_1(self, cx: &WindowContext) -> Self { elevated(self, cx, ElevationIndex::Surface) } /// See [`elevation_1`]. /// /// Renders a borderless version [`elevation_1`]. - fn elevation_1_borderless(self, cx: &mut WindowContext) -> Self { + fn elevation_1_borderless(self, cx: &WindowContext) -> Self { elevated_borderless(self, cx, ElevationIndex::Surface) } @@ -54,14 +54,14 @@ pub trait StyledExt: Styled + Sized { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels - fn elevation_2(self, cx: &mut WindowContext) -> Self { + fn elevation_2(self, cx: &WindowContext) -> Self { elevated(self, cx, ElevationIndex::ElevatedSurface) } /// See [`elevation_2`]. /// /// Renders a borderless version [`elevation_2`]. - fn elevation_2_borderless(self, cx: &mut WindowContext) -> Self { + fn elevation_2_borderless(self, cx: &WindowContext) -> Self { elevated_borderless(self, cx, ElevationIndex::ElevatedSurface) } @@ -74,24 +74,24 @@ pub trait StyledExt: Styled + Sized { /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// /// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs - fn elevation_3(self, cx: &mut WindowContext) -> Self { + fn elevation_3(self, cx: &WindowContext) -> Self { elevated(self, cx, ElevationIndex::ModalSurface) } /// See [`elevation_3`]. /// /// Renders a borderless version [`elevation_3`]. - fn elevation_3_borderless(self, cx: &mut WindowContext) -> Self { + fn elevation_3_borderless(self, cx: &WindowContext) -> Self { elevated_borderless(self, cx, ElevationIndex::ModalSurface) } /// The theme's primary border color. - fn border_primary(self, cx: &mut WindowContext) -> Self { + fn border_primary(self, cx: &WindowContext) -> Self { self.border_color(cx.theme().colors().border) } /// The theme's secondary or muted border color. - fn border_muted(self, cx: &mut WindowContext) -> Self { + fn border_muted(self, cx: &WindowContext) -> Self { self.border_color(cx.theme().colors().border_variant) }