ssh: Add UI refinements to the remote modals (#19558)

Danilo Leal created

This PR polishes spacing, borders, header design, font size, etc.

Release Notes:

- N/A

Change summary

crates/recent_projects/src/remote_servers.rs  | 124 ++++++++++++--------
crates/recent_projects/src/ssh_connections.rs |  70 +++++------
crates/vim/src/replace.rs                     |   1 
3 files changed, 106 insertions(+), 89 deletions(-)

Detailed changes

crates/recent_projects/src/remote_servers.rs 🔗

@@ -18,8 +18,8 @@ use gpui::ClipboardItem;
 use gpui::Task;
 use gpui::WeakView;
 use gpui::{
-    AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, FontWeight,
-    Model, PromptLevel, ScrollHandle, View, ViewContext,
+    AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
+    PromptLevel, ScrollHandle, View, ViewContext,
 };
 use picker::Picker;
 use project::terminals::wrap_for_ssh;
@@ -33,10 +33,10 @@ use task::HideStrategy;
 use task::RevealStrategy;
 use task::SpawnInTerminal;
 use terminal_view::terminal_panel::TerminalPanel;
-use ui::Scrollbar;
-use ui::ScrollbarState;
-use ui::Section;
-use ui::{prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Tooltip};
+use ui::{
+    prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar,
+    ScrollbarState, Section, Tooltip,
+};
 use util::ResultExt;
 use workspace::notifications::NotificationId;
 use workspace::OpenOptions;
@@ -316,7 +316,12 @@ impl gpui::Render for ProjectPicker {
                 }
                 .render(cx),
             )
-            .child(self.picker.clone())
+            .child(
+                div()
+                    .border_t_1()
+                    .border_color(cx.theme().colors().border_variant)
+                    .child(self.picker.clone()),
+            )
     }
 }
 enum Mode {
@@ -366,18 +371,34 @@ impl RemoteServerProjects {
         }
     }
 
+    fn scroll_to_selected(&self, _: &mut ViewContext<Self>) {
+        if let Mode::Default(scroll_state) = &self.mode {
+            if let ui::ScrollableHandle::NonUniform(scroll_handle) = scroll_state.scroll_handle() {
+                if let Some(active_item) = self.selectable_items.active_item {
+                    scroll_handle.scroll_to_item(active_item);
+                }
+            }
+        }
+    }
+
     fn next_item(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
         if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {
             return;
         }
         self.selectable_items.next(cx);
+        cx.notify();
+        self.scroll_to_selected(cx);
     }
+
     fn prev_item(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
         if !matches!(self.mode, Mode::Default(_) | Mode::ViewServerOptions(_, _)) {
             return;
         }
         self.selectable_items.prev(cx);
+        cx.notify();
+        self.scroll_to_selected(cx);
     }
+
     pub fn project_picker(
         ix: usize,
         connection_options: remote::SshConnectionOptions,
@@ -654,7 +675,6 @@ impl RemoteServerProjects {
                     .child(
                         Label::new(main_label)
                             .size(LabelSize::Small)
-                            .weight(FontWeight::SEMIBOLD)
                             .color(Color::Muted),
                     )
                     .children(
@@ -951,7 +971,8 @@ impl RemoteServerProjects {
             )
             .child(
                 v_flex()
-                    .py_1()
+                    .pb_1()
+                    .child(ListSeparator)
                     .child({
                         self.selectable_items.add_item(Box::new({
                             move |this, cx| {
@@ -1135,7 +1156,13 @@ impl RemoteServerProjects {
                 }
                 .render(cx),
             )
-            .child(h_flex().p_2().child(state.editor.clone()))
+            .child(
+                h_flex()
+                    .p_2()
+                    .border_t_1()
+                    .border_color(cx.theme().colors().border_variant)
+                    .child(state.editor.clone()),
+            )
     }
 
     fn render_default(
@@ -1178,26 +1205,21 @@ impl RemoteServerProjects {
             .size_full()
             .child(connect_button)
             .child(
-                h_flex().child(
-                    List::new()
-                        .empty_message(
-                            v_flex()
-                                .child(ListSeparator)
-                                .child(
-                                    div().px_3().child(
-                                        Label::new("No remote servers registered yet.")
-                                            .color(Color::Muted),
-                                    ),
-                                )
-                                .into_any_element(),
-                        )
-                        .children(ssh_connections.iter().cloned().enumerate().map(
-                            |(ix, connection)| {
-                                self.render_ssh_connection(ix, connection, cx)
-                                    .into_any_element()
-                            },
-                        )),
-                ),
+                List::new()
+                    .empty_message(
+                        v_flex()
+                            // .child(ListSeparator)
+                            .child(div().px_3().child(
+                                Label::new("No remote servers registered yet.").color(Color::Muted),
+                            ))
+                            .into_any_element(),
+                    )
+                    .children(ssh_connections.iter().cloned().enumerate().map(
+                        |(ix, connection)| {
+                            self.render_ssh_connection(ix, connection, cx)
+                                .into_any_element()
+                        },
+                    )),
             )
             .into_any_element();
 
@@ -1208,36 +1230,38 @@ impl RemoteServerProjects {
             )
             .section(
                 Section::new().padded(false).child(
-                    h_flex()
+                    v_flex()
                         .min_h(rems(20.))
+                        .group("remote-projects-section")
                         .size_full()
+                        .relative()
+                        .child(ListSeparator)
                         .child(
-                            v_flex().size_full().child(ListSeparator).child(
-                                canvas(
-                                    |bounds, cx| {
-                                        modal_section.prepaint_as_root(
-                                            bounds.origin,
-                                            bounds.size.into(),
-                                            cx,
-                                        );
-                                        modal_section
-                                    },
-                                    |_, mut modal_section, cx| {
-                                        modal_section.paint(cx);
-                                    },
-                                )
-                                .size_full(),
-                            ),
+                            canvas(
+                                |bounds, cx| {
+                                    modal_section.prepaint_as_root(
+                                        bounds.origin,
+                                        bounds.size.into(),
+                                        cx,
+                                    );
+                                    modal_section
+                                },
+                                |_, mut modal_section, cx| {
+                                    modal_section.paint(cx);
+                                },
+                            )
+                            .size_full(),
                         )
                         .child(
                             div()
+                                .visible_on_hover("remote-projects-section")
                                 .occlude()
                                 .h_full()
                                 .absolute()
-                                .right_1()
                                 .top_1()
                                 .bottom_1()
-                                .w(px(12.))
+                                .right_1()
+                                .w(px(8.))
                                 .children(Scrollbar::vertical(scroll_state)),
                         ),
                 ),
@@ -1268,6 +1292,7 @@ impl Render for RemoteServerProjects {
         div()
             .track_focus(&self.focus_handle)
             .elevation_3(cx)
+            .w(rems(34.))
             .key_context("RemoteServerModal")
             .on_action(cx.listener(Self::cancel))
             .on_action(cx.listener(Self::confirm))
@@ -1281,7 +1306,6 @@ impl Render for RemoteServerProjects {
                     cx.emit(DismissEvent)
                 }
             }))
-            .w(rems(34.))
             .child(match &self.mode {
                 Mode::Default(state) => self.render_default(state.clone(), cx).into_any_element(),
                 Mode::ViewServerOptions(index, connection) => self

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -5,7 +5,7 @@ use auto_update::AutoUpdater;
 use editor::Editor;
 use futures::channel::oneshot;
 use gpui::{
-    percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
+    percentage, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
     EventEmitter, FocusableView, ParentElement as _, PromptLevel, Render, SemanticVersion,
     SharedString, Task, TextStyleRefinement, Transformation, View,
 };
@@ -20,9 +20,8 @@ use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources};
 use theme::ThemeSettings;
 use ui::{
-    div, h_flex, prelude::*, v_flex, ActiveTheme, Color, Icon, IconName, IconSize,
-    InteractiveElement, IntoElement, Label, LabelCommon, Styled, ViewContext, VisualContext,
-    WindowContext,
+    prelude::*, ActiveTheme, Color, Icon, IconName, IconSize, InteractiveElement, IntoElement,
+    Label, LabelCommon, Styled, ViewContext, VisualContext, WindowContext,
 };
 use workspace::{AppState, ModalView, Workspace};
 
@@ -52,6 +51,7 @@ impl SshSettings {
             })
             .next()
     }
+
     pub fn nickname_for(
         &self,
         host: &str,
@@ -86,6 +86,7 @@ pub struct SshConnection {
     #[serde(default)]
     pub args: Vec<String>,
 }
+
 impl From<SshConnection> for SshConnectionOptions {
     fn from(val: SshConnection) -> Self {
         SshConnectionOptions {
@@ -205,15 +206,17 @@ impl SshPrompt {
 impl Render for SshPrompt {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let cx = cx.window_context();
-        let theme = cx.theme();
+
         v_flex()
             .key_context("PasswordPrompt")
+            .py_2()
+            .px_3()
             .size_full()
+            .text_buffer(cx)
             .when_some(self.status_message.clone(), |el, status_message| {
                 el.child(
                     h_flex()
-                        .p_2()
-                        .flex()
+                        .gap_1()
                         .child(
                             Icon::new(IconName::ArrowCircle)
                                 .size(IconSize::Medium)
@@ -225,9 +228,12 @@ impl Render for SshPrompt {
                                     },
                                 ),
                         )
-                        .child(div().ml_1().text_ellipsis().overflow_x_hidden().child(
-                            Label::new(format!("{}…", status_message)).size(LabelSize::Small),
-                        )),
+                        .child(
+                            div()
+                                .text_ellipsis()
+                                .overflow_x_hidden()
+                                .child(format!("{}…", status_message)),
+                        ),
                 )
             })
             .when_some(self.prompt.as_ref(), |el, prompt| {
@@ -235,11 +241,6 @@ impl Render for SshPrompt {
                     div()
                         .size_full()
                         .overflow_hidden()
-                        .p_4()
-                        .border_t_1()
-                        .border_color(theme.colors().border_variant)
-                        .font_buffer(cx)
-                        .text_buffer(cx)
                         .child(prompt.0.clone())
                         .child(self.editor.clone()),
                 )
@@ -292,29 +293,18 @@ impl RenderOnce for SshConnectionHeader {
         };
 
         h_flex()
-            .p_1()
+            .px(Spacing::XLarge.rems(cx))
+            .pt(Spacing::Large.rems(cx))
+            .pb(Spacing::Small.rems(cx))
             .rounded_t_md()
             .w_full()
-            .gap_2()
-            .justify_center()
-            .border_b_1()
-            .border_color(theme.colors().border_variant)
-            .bg(header_color)
+            .gap_1p5()
             .child(Icon::new(IconName::Server).size(IconSize::XSmall))
             .child(
                 h_flex()
                     .gap_1()
-                    .child(
-                        Label::new(main_label)
-                            .size(ui::LabelSize::Small)
-                            .single_line(),
-                    )
-                    .children(meta_label.map(|label| {
-                        Label::new(label)
-                            .size(ui::LabelSize::Small)
-                            .single_line()
-                            .color(Color::Muted)
-                    })),
+                    .child(Headline::new(main_label).size(HeadlineSize::XSmall))
+                    .children(meta_label.map(|label| Label::new(label).color(Color::Muted))),
             )
     }
 }
@@ -323,18 +313,18 @@ impl Render for SshConnectionModal {
     fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
         let nickname = self.prompt.read(cx).nickname.clone();
         let connection_string = self.prompt.read(cx).connection_string.clone();
-        let theme = cx.theme();
 
+        let theme = cx.theme().clone();
         let body_color = theme.colors().editor_background;
 
         v_flex()
             .elevation_3(cx)
+            .w(rems(34.))
+            .border_1()
+            .border_color(theme.colors().border)
             .track_focus(&self.focus_handle(cx))
             .on_action(cx.listener(Self::dismiss))
             .on_action(cx.listener(Self::confirm))
-            .w(px(500.))
-            .border_1()
-            .border_color(theme.colors().border)
             .child(
                 SshConnectionHeader {
                     connection_string,
@@ -343,10 +333,12 @@ impl Render for SshConnectionModal {
                 .render(cx),
             )
             .child(
-                h_flex()
-                    .rounded_b_md()
-                    .bg(body_color)
+                div()
                     .w_full()
+                    .rounded_b_lg()
+                    .bg(body_color)
+                    .border_t_1()
+                    .border_color(theme.colors().border_variant)
                     .child(self.prompt.clone()),
             )
     }

crates/vim/src/replace.rs 🔗

@@ -145,6 +145,7 @@ mod test {
     }
 
     #[gpui::test]
+    #[cfg(not(target_os = "linux"))]
     async fn test_replace_mode(cx: &mut gpui::TestAppContext) {
         let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await;