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:
parent
b752548742
commit
b64919aa11
3 changed files with 111 additions and 84 deletions
|
@ -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(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue