ssh: Overhaul remoting UI (#18727)

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
This commit is contained in:
Piotr Osiewicz 2024-10-07 15:01:50 +02:00 committed by GitHub
parent 9c5bec5efb
commit 5aa165c530
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 361 additions and 427 deletions

1
Cargo.lock generated
View file

@ -9002,7 +9002,6 @@ dependencies = [
"gpui",
"language",
"log",
"markdown",
"menu",
"ordered-float 2.10.1",
"picker",

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>

After

Width:  |  Height:  |  Size: 330 B

View file

@ -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

View file

@ -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<Workspace>,
project_path_input: View<Editor>,
dev_server_name_input: View<TextField>,
markdown: View<Markdown>,
_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::<AnyElement>(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<Self>,
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::<SshSettings>(fs, cx, move |setting, _| f(setting));
update_settings_file::<SshSettings>(fs, cx, move |setting, cx| f(setting, cx));
}
fn delete_ssh_server(&mut self, server: usize, cx: &mut ViewContext<Self>) {
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>) {
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>,
) {
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::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
.end_hover_slot::<AnyElement>(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<Self>,
) -> 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<SharedString>) -> 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<Self>) -> 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<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.p_2()
.elevation_3(cx)
.key_context("DevServerModal")
.on_action(cx.listener(Self::cancel))

View file

@ -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<Self>) -> impl IntoElement {
fn render(&mut self, _: &mut ViewContext<Self>) -> 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<Self>) -> 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()))
}
}

View file

@ -275,6 +275,7 @@ pub enum IconName {
Tab,
Terminal,
Trash,
TrashAlt,
TriangleRight,
Undo,
Unpin,

View file

@ -262,6 +262,7 @@ impl RenderOnce for ModalFooter {
#[derive(IntoElement)]
pub struct Section {
contained: bool,
padded: bool,
header: Option<SectionHeader>,
meta: Option<SharedString>,
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)
};

View file

@ -3,7 +3,7 @@ use gpui::{hsla, Styled, WindowContext};
use crate::prelude::*;
use crate::ElevationIndex;
fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
fn elevated<E: Styled>(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<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -
.shadow(index.shadow())
}
fn elevated_borderless<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
fn elevated_borderless<E: Styled>(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)
}