ssh: Refine the modal UI (#19256)

This PR refines the SSH modal UI, adjusting spacing and alignment. Via
these changes, I'm also introducing the ability for the `empty_message`
on the `List` component to receive not just a string but any element.
The custom way in which the SSH modal was designed made it feel like
this was needed for proper spacing.

<img width="700" alt="Screenshot 2024-10-16 at 1 20 54 AM"
src="https://github.com/user-attachments/assets/f2e0586b-4c9f-4497-b4cb-e90c8157512b">


Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2024-10-16 01:39:27 +02:00 committed by GitHub
parent b752548742
commit b64919aa11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 111 additions and 84 deletions

View file

@ -35,7 +35,7 @@ use task::RevealStrategy;
use task::SpawnInTerminal; use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel; use terminal_view::terminal_panel::TerminalPanel;
use ui::Section; use ui::Section;
use ui::{prelude::*, List, ListItem, ListSeparator, Modal, ModalHeader, Tooltip}; use ui::{prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Tooltip};
use util::ResultExt; use util::ResultExt;
use workspace::notifications::NotificationId; use workspace::notifications::NotificationId;
use workspace::OpenOptions; use workspace::OpenOptions;
@ -604,19 +604,16 @@ impl DevServerProjects {
}; };
v_flex() v_flex()
.w_full() .w_full()
.border_b_1() .child(ListSeparator)
.border_color(cx.theme().colors().border_variant)
.mb_1()
.child( .child(
h_flex() h_flex()
.group("ssh-server") .group("ssh-server")
.w_full() .w_full()
.pt_0p5() .pt_0p5()
.px_2p5() .px_3()
.gap_1() .gap_1()
.overflow_hidden() .overflow_hidden()
.whitespace_nowrap() .whitespace_nowrap()
.w_full()
.child( .child(
Label::new(main_label) Label::new(main_label)
.size(LabelSize::Small) .size(LabelSize::Small)
@ -630,68 +627,63 @@ impl DevServerProjects {
), ),
) )
.child( .child(
v_flex().w_full().gap_1().mb_1().child( List::new()
List::new() .empty_message("No projects.")
.empty_message("No projects.") .children(ssh_connection.projects.iter().enumerate().map(|(pix, p)| {
.children(ssh_connection.projects.iter().enumerate().map(|(pix, p)| { v_flex().gap_0p5().child(self.render_ssh_project(
v_flex().gap_0p5().child(self.render_ssh_project( ix,
ix, &ssh_connection,
&ssh_connection, pix,
pix, p,
p, cx,
cx, ))
)) }))
})) .child(h_flex().map(|this| {
.child(h_flex().map(|this| { self.selectable_items.add_item(Box::new({
self.selectable_items.add_item(Box::new({ let ssh_connection = ssh_connection.clone();
let ssh_connection = ssh_connection.clone(); move |this, cx| {
move |this, cx| { this.create_ssh_project(ix, ssh_connection.clone(), cx);
this.create_ssh_project(ix, ssh_connection.clone(), cx); }
} }));
})); let is_selected = self.selectable_items.is_selected();
let is_selected = self.selectable_items.is_selected(); this.child(
this.child( ListItem::new(("new-remote-project", ix))
ListItem::new(("new-remote-project", ix)) .selected(is_selected)
.selected(is_selected) .inset(true)
.inset(true) .spacing(ui::ListItemSpacing::Sparse)
.spacing(ui::ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::Plus).color(Color::Muted))
.start_slot(Icon::new(IconName::Plus).color(Color::Muted)) .child(Label::new("Open Folder"))
.child(Label::new("Open Folder")) .on_click(cx.listener({
.on_click(cx.listener({ let ssh_connection = ssh_connection.clone();
let ssh_connection = ssh_connection.clone(); move |this, _, cx| {
move |this, _, cx| { this.create_ssh_project(ix, ssh_connection.clone(), cx);
this.create_ssh_project(ix, ssh_connection.clone(), cx); }
} })),
})), )
) }))
})) .child(h_flex().map(|this| {
.child(h_flex().map(|this| { self.selectable_items.add_item(Box::new({
self.selectable_items.add_item(Box::new({ let ssh_connection = ssh_connection.clone();
let ssh_connection = ssh_connection.clone(); move |this, cx| {
move |this, cx| { this.view_server_options((ix, ssh_connection.clone()), cx);
this.view_server_options((ix, ssh_connection.clone()), cx); }
} }));
})); let is_selected = self.selectable_items.is_selected();
let is_selected = self.selectable_items.is_selected(); this.child(
this.child( ListItem::new(("server-options", ix))
ListItem::new(("server-options", ix)) .selected(is_selected)
.selected(is_selected) .inset(true)
.inset(true) .spacing(ui::ListItemSpacing::Sparse)
.spacing(ui::ListItemSpacing::Sparse) .start_slot(Icon::new(IconName::Settings).color(Color::Muted))
.start_slot(Icon::new(IconName::Settings).color(Color::Muted)) .child(Label::new("View Server Options"))
.child(Label::new("View Server Options")) .on_click(cx.listener({
.on_click(cx.listener({ let ssh_connection = ssh_connection.clone();
let ssh_connection = ssh_connection.clone(); move |this, _, cx| {
move |this, _, cx| { this.view_server_options((ix, ssh_connection.clone()), cx);
this.view_server_options( }
(ix, ssh_connection.clone()), })),
cx, )
); })),
}
})),
)
})),
),
) )
} }
@ -762,6 +754,7 @@ impl DevServerProjects {
.end_hover_slot::<AnyElement>(Some( .end_hover_slot::<AnyElement>(Some(
IconButton::new("remove-remote-project", IconName::TrashAlt) IconButton::new("remove-remote-project", IconName::TrashAlt)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.shape(IconButtonShape::Square)
.on_click( .on_click(
cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)), cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)),
) )
@ -1117,6 +1110,7 @@ impl DevServerProjects {
})); }));
let is_selected = self.selectable_items.is_selected(); let is_selected = self.selectable_items.is_selected();
let connect_button = ListItem::new("register-dev-server-button") let connect_button = ListItem::new("register-dev-server-button")
.selected(is_selected) .selected(is_selected)
.inset(true) .inset(true)
@ -1130,16 +1124,21 @@ impl DevServerProjects {
cx.notify(); cx.notify();
})); }));
let footer = format!("Servers: {}", ssh_connections.len() + dev_servers.len());
let mut modal_section = v_flex() let mut modal_section = v_flex()
.id("ssh-server-list") .id("ssh-server-list")
.overflow_y_scroll() .overflow_y_scroll()
.size_full() .size_full()
.child(connect_button) .child(connect_button)
.child(ListSeparator)
.child( .child(
List::new() List::new()
.empty_message("No dev servers registered yet.") .empty_message(
v_flex()
.child(ListSeparator)
.child(div().px_3().child(
Label::new("No dev servers registered yet.").color(Color::Muted),
))
.into_any_element(),
)
.children(ssh_connections.iter().cloned().enumerate().map( .children(ssh_connections.iter().cloned().enumerate().map(
|(ix, connection)| { |(ix, connection)| {
self.render_ssh_connection(ix, connection, cx) self.render_ssh_connection(ix, connection, cx)
@ -1149,23 +1148,25 @@ impl DevServerProjects {
) )
.into_any_element(); .into_any_element();
let server_count = format!("Servers: {}", ssh_connections.len() + dev_servers.len());
Modal::new("remote-projects", Some(self.scroll_handle.clone())) Modal::new("remote-projects", Some(self.scroll_handle.clone()))
.header( .header(
ModalHeader::new().child( ModalHeader::new().child(
h_flex() h_flex()
.items_center()
.justify_between() .justify_between()
.child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall)) .child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall))
.child(Label::new(footer).size(LabelSize::Small)), .child(Label::new(server_count).size(LabelSize::Small)),
), ),
) )
.section( .section(
Section::new().padded(false).child( Section::new().padded(false).child(
v_flex() v_flex()
.min_h(rems(28.)) .min_h(rems(20.))
.flex_1()
.size_full() .size_full()
.pt_1p5() .child(ListSeparator)
.border_y_1()
.border_color(cx.theme().colors().border_variant)
.child( .child(
canvas( canvas(
|bounds, cx| { |bounds, cx| {
@ -1180,9 +1181,7 @@ impl DevServerProjects {
modal_section.paint(cx); modal_section.paint(cx);
}, },
) )
.size_full() .size_full(),
.min_h_full()
.flex_1(),
), ),
), ),
) )

View file

@ -5,11 +5,16 @@ use smallvec::SmallVec;
use crate::{prelude::*, v_flex, Label, ListHeader}; use crate::{prelude::*, v_flex, Label, ListHeader};
pub enum EmptyMessage {
Text(SharedString),
Element(AnyElement),
}
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct List { pub struct List {
/// Message to display when the list is empty /// Message to display when the list is empty
/// Defaults to "No items" /// Defaults to "No items"
empty_message: SharedString, empty_message: EmptyMessage,
header: Option<ListHeader>, header: Option<ListHeader>,
toggle: Option<bool>, toggle: Option<bool>,
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
@ -24,15 +29,15 @@ impl Default for List {
impl List { impl List {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
empty_message: "No items".into(), empty_message: EmptyMessage::Text("No items".into()),
header: None, header: None,
toggle: None, toggle: None,
children: SmallVec::new(), children: SmallVec::new(),
} }
} }
pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self { pub fn empty_message(mut self, message: impl Into<EmptyMessage>) -> Self {
self.empty_message = empty_message.into(); self.empty_message = message.into();
self self
} }
@ -53,6 +58,24 @@ impl ParentElement for List {
} }
} }
impl From<String> for EmptyMessage {
fn from(s: String) -> Self {
EmptyMessage::Text(SharedString::from(s))
}
}
impl From<&str> for EmptyMessage {
fn from(s: &str) -> Self {
EmptyMessage::Text(SharedString::from(s.to_owned()))
}
}
impl From<AnyElement> for EmptyMessage {
fn from(e: AnyElement) -> Self {
EmptyMessage::Element(e)
}
}
impl RenderOnce for List { impl RenderOnce for List {
fn render(self, cx: &mut WindowContext) -> impl IntoElement { fn render(self, cx: &mut WindowContext) -> impl IntoElement {
v_flex() v_flex()
@ -62,7 +85,10 @@ impl RenderOnce for List {
.map(|this| match (self.children.is_empty(), self.toggle) { .map(|this| match (self.children.is_empty(), self.toggle) {
(false, _) => this.children(self.children), (false, _) => this.children(self.children),
(true, Some(false)) => this, (true, Some(false)) => this,
(true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)), (true, _) => match self.empty_message {
EmptyMessage::Text(text) => this.child(Label::new(text).color(Color::Muted)),
EmptyMessage::Element(element) => this.child(element),
},
}) })
} }
} }

View file

@ -77,6 +77,7 @@ impl RenderOnce for Modal {
v_flex() v_flex()
.id(self.container_id.clone()) .id(self.container_id.clone())
.w_full() .w_full()
.flex_1()
.gap(Spacing::Large.rems(cx)) .gap(Spacing::Large.rems(cx))
.when_some( .when_some(
self.container_scroll_handler, self.container_scroll_handler,
@ -344,6 +345,7 @@ impl RenderOnce for Section {
} else { } else {
v_flex() v_flex()
.w_full() .w_full()
.flex_1()
.gap_y(Spacing::Small.rems(cx)) .gap_y(Spacing::Small.rems(cx))
.when(self.padded, |this| { .when(self.padded, |this| {
this.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx)) this.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx))