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:
parent
9c5bec5efb
commit
5aa165c530
8 changed files with 361 additions and 427 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9002,7 +9002,6 @@ dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
"log",
|
"log",
|
||||||
"markdown",
|
|
||||||
"menu",
|
"menu",
|
||||||
"ordered-float 2.10.1",
|
"ordered-float 2.10.1",
|
||||||
"picker",
|
"picker",
|
||||||
|
|
1
assets/icons/trash_alt.svg
Normal file
1
assets/icons/trash_alt.svg
Normal 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 |
|
@ -22,7 +22,6 @@ futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
markdown.workspace = true
|
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
|
|
|
@ -8,17 +8,18 @@ use anyhow::Result;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
|
use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
|
use gpui::pulsating_between;
|
||||||
use gpui::AsyncWindowContext;
|
use gpui::AsyncWindowContext;
|
||||||
|
use gpui::ClipboardItem;
|
||||||
use gpui::PathPromptOptions;
|
use gpui::PathPromptOptions;
|
||||||
use gpui::Subscription;
|
use gpui::Subscription;
|
||||||
use gpui::Task;
|
use gpui::Task;
|
||||||
use gpui::WeakView;
|
use gpui::WeakView;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
percentage, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter,
|
percentage, Action, Animation, AnimationExt, AnyElement, AppContext, DismissEvent,
|
||||||
FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext,
|
EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View,
|
||||||
|
ViewContext,
|
||||||
};
|
};
|
||||||
use markdown::Markdown;
|
|
||||||
use markdown::MarkdownStyle;
|
|
||||||
use project::terminals::wrap_for_ssh;
|
use project::terminals::wrap_for_ssh;
|
||||||
use project::terminals::SshCommand;
|
use project::terminals::SshCommand;
|
||||||
use rpc::proto::RegenerateDevServerTokenResponse;
|
use rpc::proto::RegenerateDevServerTokenResponse;
|
||||||
|
@ -35,8 +36,8 @@ use terminal_view::terminal_panel::TerminalPanel;
|
||||||
use ui::ElevationIndex;
|
use ui::ElevationIndex;
|
||||||
use ui::Section;
|
use ui::Section;
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, Indicator, List, ListHeader, ListItem, Modal, ModalFooter, ModalHeader,
|
prelude::*, IconButtonShape, Indicator, List, ListItem, Modal, ModalFooter, ModalHeader,
|
||||||
RadioWithLabel, Tooltip,
|
Tooltip,
|
||||||
};
|
};
|
||||||
use ui_input::{FieldLabelLayout, TextField};
|
use ui_input::{FieldLabelLayout, TextField};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -62,7 +63,6 @@ pub struct DevServerProjects {
|
||||||
workspace: WeakView<Workspace>,
|
workspace: WeakView<Workspace>,
|
||||||
project_path_input: View<Editor>,
|
project_path_input: View<Editor>,
|
||||||
dev_server_name_input: View<TextField>,
|
dev_server_name_input: View<TextField>,
|
||||||
markdown: View<Markdown>,
|
|
||||||
_dev_server_subscription: Subscription,
|
_dev_server_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,26 +132,6 @@ impl DevServerProjects {
|
||||||
..Default::default()
|
..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 {
|
Self {
|
||||||
mode: Mode::Default(None),
|
mode: Mode::Default(None),
|
||||||
focus_handle,
|
focus_handle,
|
||||||
|
@ -159,7 +139,6 @@ impl DevServerProjects {
|
||||||
dev_server_store,
|
dev_server_store,
|
||||||
project_path_input,
|
project_path_input,
|
||||||
dev_server_name_input,
|
dev_server_name_input,
|
||||||
markdown,
|
|
||||||
workspace,
|
workspace,
|
||||||
_dev_server_subscription: subscription,
|
_dev_server_subscription: subscription,
|
||||||
}
|
}
|
||||||
|
@ -845,7 +824,7 @@ impl DevServerProjects {
|
||||||
})
|
})
|
||||||
.child({
|
.child({
|
||||||
let dev_server_id = dev_server.id;
|
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| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.delete_dev_server(dev_server_id, cx)
|
this.delete_dev_server(dev_server_id, cx)
|
||||||
}))
|
}))
|
||||||
|
@ -913,40 +892,73 @@ impl DevServerProjects {
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
.px(Spacing::Small.rems(cx) + Spacing::Small.rems(cx))
|
||||||
.child(
|
.child(
|
||||||
h_flex().group("ssh-server").justify_between().child(
|
h_flex()
|
||||||
h_flex()
|
.w_full()
|
||||||
.gap_2()
|
.group("ssh-server")
|
||||||
.child(
|
.justify_between()
|
||||||
div()
|
.child(
|
||||||
.id(("status", ix))
|
h_flex()
|
||||||
.relative()
|
.gap_2()
|
||||||
.child(Icon::new(IconName::Server).size(IconSize::Small)),
|
.w_full()
|
||||||
)
|
.child(
|
||||||
.child(
|
div()
|
||||||
div()
|
.id(("status", ix))
|
||||||
.max_w(rems(26.))
|
.relative()
|
||||||
.overflow_hidden()
|
.child(Icon::new(IconName::Server).size(IconSize::Small)),
|
||||||
.whitespace_nowrap()
|
)
|
||||||
.child(Label::new(ssh_connection.host.clone())),
|
.child(
|
||||||
)
|
h_flex()
|
||||||
.child(h_flex().visible_on_hover("ssh-server").gap_1().child({
|
.max_w(rems(26.))
|
||||||
IconButton::new("remove-dev-server", IconName::Trash)
|
.overflow_hidden()
|
||||||
.on_click(
|
.whitespace_nowrap()
|
||||||
cx.listener(move |this, _, cx| this.delete_ssh_server(ix, cx)),
|
.child(Label::new(ssh_connection.host.clone())),
|
||||||
)
|
),
|
||||||
.tooltip(|cx| Tooltip::text("Remove Dev Server", cx))
|
)
|
||||||
})),
|
.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(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.bg(cx.theme().colors().background)
|
.border_l_1()
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().colors().border_variant)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
.rounded_md()
|
|
||||||
.my_1()
|
.my_1()
|
||||||
|
.mx_1p5()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.px_3()
|
.px_3()
|
||||||
.child(
|
.child(
|
||||||
|
@ -956,12 +968,17 @@ impl DevServerProjects {
|
||||||
self.render_ssh_project(ix, &ssh_connection, pix, p, cx)
|
self.render_ssh_project(ix, &ssh_connection, pix, p, cx)
|
||||||
}))
|
}))
|
||||||
.child(
|
.child(
|
||||||
ListItem::new("new-remote_project")
|
h_flex().child(
|
||||||
.start_slot(Icon::new(IconName::Plus))
|
Button::new("new-remote_project", "Open Folder…")
|
||||||
.child(Label::new("Open folder…"))
|
.icon(IconName::Plus)
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.size(ButtonSize::Default)
|
||||||
this.create_ssh_project(ix, ssh_connection.clone(), cx);
|
.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 project = project.clone();
|
||||||
let server = server.clone();
|
let server = server.clone();
|
||||||
ListItem::new(("remote-project", ix))
|
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(", ")))
|
.child(Label::new(project.paths.join(", ")))
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
let Some(app_state) = this
|
let Some(app_state) = this
|
||||||
|
@ -1014,7 +1032,7 @@ impl DevServerProjects {
|
||||||
.detach();
|
.detach();
|
||||||
}))
|
}))
|
||||||
.end_hover_slot::<AnyElement>(Some(
|
.end_hover_slot::<AnyElement>(Some(
|
||||||
IconButton::new("remove-remote-project", IconName::Trash)
|
IconButton::new("remove-remote-project", IconName::TrashAlt)
|
||||||
.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)),
|
||||||
)
|
)
|
||||||
|
@ -1026,7 +1044,7 @@ impl DevServerProjects {
|
||||||
fn update_settings_file(
|
fn update_settings_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
cx: &mut ViewContext<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
|
let Some(fs) = self
|
||||||
.workspace
|
.workspace
|
||||||
|
@ -1035,11 +1053,11 @@ impl DevServerProjects {
|
||||||
else {
|
else {
|
||||||
return;
|
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>) {
|
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() {
|
if let Some(connections) = setting.ssh_connections.as_mut() {
|
||||||
connections.remove(server);
|
connections.remove(server);
|
||||||
}
|
}
|
||||||
|
@ -1047,7 +1065,7 @@ impl DevServerProjects {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext<Self>) {
|
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
|
if let Some(server) = setting
|
||||||
.ssh_connections
|
.ssh_connections
|
||||||
.as_mut()
|
.as_mut()
|
||||||
|
@ -1063,7 +1081,7 @@ impl DevServerProjects {
|
||||||
connection_options: remote::SshConnectionOptions,
|
connection_options: remote::SshConnectionOptions,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.update_settings_file(cx, move |setting| {
|
self.update_settings_file(cx, move |setting, _| {
|
||||||
setting
|
setting
|
||||||
.ssh_connections
|
.ssh_connections
|
||||||
.get_or_insert(Default::default())
|
.get_or_insert(Default::default())
|
||||||
|
@ -1124,7 +1142,7 @@ impl DevServerProjects {
|
||||||
}).detach();
|
}).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| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.delete_dev_server_project(dev_server_project_id, cx)
|
this.delete_dev_server_project(dev_server_project_id, cx)
|
||||||
}))
|
}))
|
||||||
|
@ -1148,250 +1166,109 @@ impl DevServerProjects {
|
||||||
kind = NewServerKind::DirectSSH;
|
kind = NewServerKind::DirectSSH;
|
||||||
}
|
}
|
||||||
|
|
||||||
let status = dev_server_id
|
self.dev_server_name_input.update(cx, |input, cx| {
|
||||||
.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| {
|
|
||||||
input.editor().update(cx, |editor, cx| {
|
input.editor().update(cx, |editor, cx| {
|
||||||
if editor.text(cx).is_empty() {
|
if editor.text(cx).is_empty() {
|
||||||
match kind {
|
editor.set_placeholder_text("ssh me@my.server / ssh@secret-box:2222", cx);
|
||||||
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.text(cx)
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
let theme = cx.theme();
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.pl_2()
|
.id("create-dev-server")
|
||||||
.pt_2()
|
.overflow_hidden()
|
||||||
.gap_2()
|
.size_full()
|
||||||
.child(v_flex().w_full().text_sm().child(self.markdown.clone()))
|
.flex_1()
|
||||||
.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()
|
|
||||||
.child(
|
.child(
|
||||||
Icon::new(IconName::ArrowCircle)
|
h_flex()
|
||||||
.size(IconSize::Medium)
|
.p_2()
|
||||||
.with_animation(
|
.gap_2()
|
||||||
"arrow-circle",
|
.items_center()
|
||||||
Animation::new(Duration::from_secs(2)).repeat(),
|
.border_b_1()
|
||||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
.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 {
|
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
@ -1416,64 +1293,73 @@ impl DevServerProjects {
|
||||||
creating_dev_server = Some(*dev_server_id);
|
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()))
|
Modal::new("remote-projects", Some(self.scroll_handle.clone()))
|
||||||
.header(
|
.header(
|
||||||
ModalHeader::new()
|
ModalHeader::new().child(
|
||||||
.show_dismiss_button(true)
|
h_flex()
|
||||||
.child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::Small)),
|
.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(
|
||||||
Section::new().child(
|
Section::new().padded(false).child(
|
||||||
div().child(
|
div()
|
||||||
List::new()
|
.border_y_1()
|
||||||
.empty_message("No dev servers registered yet.")
|
.border_color(cx.theme().colors().border_variant)
|
||||||
.header(Some(
|
.w_full()
|
||||||
ListHeader::new("Connections").end_slot(
|
.child(
|
||||||
Button::new("register-dev-server-button", "Connect New Server")
|
div().p_2().child(
|
||||||
.icon(IconName::Plus)
|
List::new()
|
||||||
.icon_position(IconPosition::Start)
|
.empty_message("No dev servers registered yet.")
|
||||||
.icon_color(Color::Muted)
|
.children(ssh_connections.iter().cloned().enumerate().map(
|
||||||
.on_click(cx.listener(|this, _, cx| {
|
|(ix, connection)| {
|
||||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
self.render_ssh_connection(ix, connection, cx)
|
||||||
kind: if SshSettings::get_global(cx)
|
.into_any_element()
|
||||||
.use_direct_ssh()
|
},
|
||||||
{
|
))
|
||||||
NewServerKind::DirectSSH
|
.children(dev_servers.iter().map(|dev_server| {
|
||||||
} else {
|
let creating = if creating_dev_server == Some(dev_server.id)
|
||||||
NewServerKind::LegacySSH
|
{
|
||||||
},
|
is_creating
|
||||||
..Default::default()
|
} else {
|
||||||
});
|
None
|
||||||
this.dev_server_name_input.update(
|
};
|
||||||
cx,
|
self.render_dev_server(dev_server, creating, cx)
|
||||||
|text_field, cx| {
|
.into_any_element()
|
||||||
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()
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.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 {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
div()
|
div()
|
||||||
.track_focus(&self.focus_handle)
|
.track_focus(&self.focus_handle)
|
||||||
.p_2()
|
|
||||||
.elevation_3(cx)
|
.elevation_3(cx)
|
||||||
.key_context("DevServerModal")
|
.key_context("DevServerModal")
|
||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::cancel))
|
||||||
|
|
|
@ -5,9 +5,9 @@ use auto_update::AutoUpdater;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
|
percentage, px, Action, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext,
|
||||||
EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task,
|
DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion,
|
||||||
Transformation, View,
|
SharedString, Task, Transformation, View,
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, Model};
|
use gpui::{AppContext, Model};
|
||||||
use release_channel::{AppVersion, ReleaseChannel};
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
|
@ -16,9 +16,9 @@ use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsSources};
|
use settings::{Settings, SettingsSources};
|
||||||
use ui::{
|
use ui::{
|
||||||
h_flex, v_flex, Color, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement,
|
div, h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, FluentBuilder as _, Icon,
|
||||||
IntoElement, Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext,
|
IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, Styled,
|
||||||
WindowContext,
|
StyledExt as _, Tooltip, ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use workspace::{AppState, ModalView, Workspace};
|
use workspace::{AppState, ModalView, Workspace};
|
||||||
|
|
||||||
|
@ -140,47 +140,57 @@ impl SshPrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for 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()
|
v_flex()
|
||||||
|
.w_full()
|
||||||
.key_context("PasswordPrompt")
|
.key_context("PasswordPrompt")
|
||||||
.p_4()
|
.justify_start()
|
||||||
.size_full()
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
v_flex()
|
||||||
.gap_2()
|
.p_4()
|
||||||
.child(if self.error_message.is_some() {
|
.size_full()
|
||||||
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(
|
.child(
|
||||||
Label::new(format!("ssh {}…", self.connection_string))
|
h_flex()
|
||||||
.size(ui::LabelSize::Large),
|
.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 {
|
impl Render for SshConnectionModal {
|
||||||
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
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()
|
v_flex()
|
||||||
.elevation_3(cx)
|
.elevation_3(cx)
|
||||||
.p_4()
|
|
||||||
.gap_2()
|
|
||||||
.on_action(cx.listener(Self::dismiss))
|
.on_action(cx.listener(Self::dismiss))
|
||||||
.on_action(cx.listener(Self::confirm))
|
.on_action(cx.listener(Self::confirm))
|
||||||
.w(px(400.))
|
.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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -275,6 +275,7 @@ pub enum IconName {
|
||||||
Tab,
|
Tab,
|
||||||
Terminal,
|
Terminal,
|
||||||
Trash,
|
Trash,
|
||||||
|
TrashAlt,
|
||||||
TriangleRight,
|
TriangleRight,
|
||||||
Undo,
|
Undo,
|
||||||
Unpin,
|
Unpin,
|
||||||
|
|
|
@ -262,6 +262,7 @@ impl RenderOnce for ModalFooter {
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Section {
|
pub struct Section {
|
||||||
contained: bool,
|
contained: bool,
|
||||||
|
padded: bool,
|
||||||
header: Option<SectionHeader>,
|
header: Option<SectionHeader>,
|
||||||
meta: Option<SharedString>,
|
meta: Option<SharedString>,
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
@ -277,6 +278,7 @@ impl Section {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
contained: false,
|
contained: false,
|
||||||
|
padded: true,
|
||||||
header: None,
|
header: None,
|
||||||
meta: None,
|
meta: None,
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
|
@ -286,6 +288,7 @@ impl Section {
|
||||||
pub fn new_contained() -> Self {
|
pub fn new_contained() -> Self {
|
||||||
Self {
|
Self {
|
||||||
contained: true,
|
contained: true,
|
||||||
|
padded: true,
|
||||||
header: None,
|
header: None,
|
||||||
meta: None,
|
meta: None,
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
|
@ -306,6 +309,10 @@ impl Section {
|
||||||
self.meta = Some(meta.into());
|
self.meta = Some(meta.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
pub fn padded(mut self, padded: bool) -> Self {
|
||||||
|
self.padded = padded;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParentElement for Section {
|
impl ParentElement for Section {
|
||||||
|
@ -320,22 +327,27 @@ impl RenderOnce for Section {
|
||||||
section_bg.fade_out(0.96);
|
section_bg.fade_out(0.96);
|
||||||
|
|
||||||
let children = if self.contained {
|
let children = if self.contained {
|
||||||
v_flex().flex_1().px(Spacing::XLarge.rems(cx)).child(
|
v_flex()
|
||||||
v_flex()
|
.flex_1()
|
||||||
.w_full()
|
.when(self.padded, |this| this.px(Spacing::XLarge.rems(cx)))
|
||||||
.rounded_md()
|
.child(
|
||||||
.border_1()
|
v_flex()
|
||||||
.border_color(cx.theme().colors().border)
|
.w_full()
|
||||||
.bg(section_bg)
|
.rounded_md()
|
||||||
.py(Spacing::Medium.rems(cx))
|
.border_1()
|
||||||
.gap_y(Spacing::Small.rems(cx))
|
.border_color(cx.theme().colors().border)
|
||||||
.child(div().flex().flex_1().size_full().children(self.children)),
|
.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 {
|
} else {
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_y(Spacing::Small.rems(cx))
|
.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)
|
.children(self.children)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use gpui::{hsla, Styled, WindowContext};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::ElevationIndex;
|
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)
|
this.bg(cx.theme().colors().elevated_surface_background)
|
||||||
.rounded_lg()
|
.rounded_lg()
|
||||||
.border_1()
|
.border_1()
|
||||||
|
@ -11,7 +11,7 @@ fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -
|
||||||
.shadow(index.shadow())
|
.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)
|
this.bg(cx.theme().colors().elevated_surface_background)
|
||||||
.rounded_lg()
|
.rounded_lg()
|
||||||
.shadow(index.shadow())
|
.shadow(index.shadow())
|
||||||
|
@ -38,14 +38,14 @@ pub trait StyledExt: Styled + Sized {
|
||||||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||||
///
|
///
|
||||||
/// Example Elements: Title Bar, Panel, Tab Bar, Editor
|
/// 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)
|
elevated(self, cx, ElevationIndex::Surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [`elevation_1`].
|
/// See [`elevation_1`].
|
||||||
///
|
///
|
||||||
/// Renders a borderless version [`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)
|
elevated_borderless(self, cx, ElevationIndex::Surface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,14 +54,14 @@ pub trait StyledExt: Styled + Sized {
|
||||||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||||
///
|
///
|
||||||
/// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels
|
/// 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)
|
elevated(self, cx, ElevationIndex::ElevatedSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [`elevation_2`].
|
/// See [`elevation_2`].
|
||||||
///
|
///
|
||||||
/// Renders a borderless version [`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)
|
elevated_borderless(self, cx, ElevationIndex::ElevatedSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,24 +74,24 @@ pub trait StyledExt: Styled + Sized {
|
||||||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||||
///
|
///
|
||||||
/// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs
|
/// 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)
|
elevated(self, cx, ElevationIndex::ModalSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [`elevation_3`].
|
/// See [`elevation_3`].
|
||||||
///
|
///
|
||||||
/// Renders a borderless version [`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)
|
elevated_borderless(self, cx, ElevationIndex::ModalSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The theme's primary border color.
|
/// 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)
|
self.border_color(cx.theme().colors().border)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The theme's secondary or muted border color.
|
/// 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)
|
self.border_color(cx.theme().colors().border_variant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue