mirror of
https://github.com/nyakang/nyaterm.git
synced 2026-10-05 00:01:00 +00:00
feat(connections): give the connection editor real input fields
Every input in the new/edit-connection dialog was a label div sharing one focus handle, with a `focused_field` enum routing keystrokes to whichever draft string was "active". Clicking a field only moved that enum, so there was no caret, no selection, no pointer positioning, no composition, and password masking was a string of bullets rebuilt on each render. Each text input is now a `TextField` entity, built when the editor opens and cleared when it closes, so the widgets live exactly as long as the draft they mirror. `ConnectionEditorState` stays a plain value the runtime can clone and save from; the fields write back through their subscriptions. The password field masks itself while the buffer keeps the secret in the clear, which is what the save path needs. The folder-name editor gets the same treatment. Two focus fixes fall out of this. The editor surface's blanket `on_click` focus grab is gone: it existed to keep the old fake inputs "focused", and would now steal focus back from whichever field the pointer just landed on, since click follows mouse-down. And because focus is per-window, the detached editor window claims the name field itself on its first frame rather than trusting what the main window focused. The labelled row is the hit target, not just the one text line inside it, and it takes its ring from the widget's own focus rather than from an enum nothing updates any more.
This commit is contained in:
@@ -4,10 +4,14 @@ use gpui::{
|
||||
};
|
||||
|
||||
use super::{NyaTermApp, child_window_header, child_window_titlebar};
|
||||
use crate::models::ConnectionEditorField;
|
||||
|
||||
pub(in crate::features) struct ConnectionEditorWindow {
|
||||
app: Entity<NyaTermApp>,
|
||||
_app_subscription: Subscription,
|
||||
/// Focus is per-window, so the field the main window focused means nothing
|
||||
/// here; this window has to claim it on its own first frame.
|
||||
focused_initial_field: bool,
|
||||
}
|
||||
|
||||
impl ConnectionEditorWindow {
|
||||
@@ -16,6 +20,7 @@ impl ConnectionEditorWindow {
|
||||
Self {
|
||||
app,
|
||||
_app_subscription: app_subscription,
|
||||
focused_initial_field: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,6 +45,20 @@ impl Render for ConnectionEditorWindow {
|
||||
)
|
||||
});
|
||||
window.set_window_title(&title);
|
||||
if !self.focused_initial_field {
|
||||
self.focused_initial_field = true;
|
||||
let field = self
|
||||
.app
|
||||
.read(cx)
|
||||
.connection_state
|
||||
.editor_fields()
|
||||
.get(&ConnectionEditorField::Name)
|
||||
.cloned();
|
||||
if let Some(field) = field {
|
||||
window.focus(&field.read(cx).focus_handle());
|
||||
field.update(cx, |field, cx| field.select_all(window, cx));
|
||||
}
|
||||
}
|
||||
let content = self
|
||||
.app
|
||||
.update(cx, |app, cx| app.connection_editor_window_view(editor, cx));
|
||||
|
||||
@@ -102,16 +102,33 @@ impl NyaTermApp {
|
||||
};
|
||||
|
||||
self.connection_state.editor.begin_edit(editor);
|
||||
// Fields mirror the draft, so they are rebuilt with it.
|
||||
self.connection_state.build_editor_fields(cx);
|
||||
self.terminal.view.status = "connection editor opened".to_string();
|
||||
if !self.open_connection_editor_window(cx) {
|
||||
let editor_focus = self.connection_state.editor.focus_handle();
|
||||
window.focus(&editor_focus);
|
||||
// Land on the name and select it, so an edit can start by typing.
|
||||
match self
|
||||
.connection_state
|
||||
.editor_fields()
|
||||
.get(&ConnectionEditorField::Name)
|
||||
.cloned()
|
||||
{
|
||||
Some(field) => {
|
||||
window.focus(&field.read(cx).focus_handle());
|
||||
field.update(cx, |field, cx| field.select_all(window, cx));
|
||||
}
|
||||
None => {
|
||||
let editor_focus = self.connection_state.editor.focus_handle();
|
||||
window.focus(&editor_focus);
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(in crate::features) fn close_connection_editor(&mut self, cx: &mut Context<Self>) {
|
||||
self.connection_state.editor.close();
|
||||
self.connection_state.clear_editor_fields();
|
||||
self.terminal.view.status = "connection editor closed".to_string();
|
||||
cx.notify();
|
||||
}
|
||||
@@ -188,6 +205,17 @@ impl NyaTermApp {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Take an edit from a field widget into the draft.
|
||||
pub(in crate::features) fn apply_connection_editor_field_text(
|
||||
&mut self,
|
||||
field: ConnectionEditorField,
|
||||
text: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.connection_state.set_editor_field_text(field, text);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(in crate::features) fn focus_connection_editor_field(
|
||||
&mut self,
|
||||
field: ConnectionEditorField,
|
||||
|
||||
@@ -43,14 +43,17 @@ impl NyaTermApp {
|
||||
parent_id,
|
||||
error: None,
|
||||
});
|
||||
self.connection_state.build_group_editor_field(cx);
|
||||
self.terminal.view.status = "connection group editor opened".to_string();
|
||||
let group_editor_focus = self.connection_state.group_editor.focus_handle();
|
||||
window.focus(&group_editor_focus);
|
||||
if let Some(field) = self.connection_state.group_editor_field() {
|
||||
window.focus(&field.read(cx).focus_handle());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(in crate::features) fn close_connection_group_editor(&mut self, cx: &mut Context<Self>) {
|
||||
self.connection_state.group_editor.close();
|
||||
self.connection_state.clear_group_editor_field();
|
||||
self.terminal.view.status = "connection group editor closed".to_string();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Entity, FocusHandle, Pixels, SharedString, Subscription,
|
||||
@@ -30,9 +30,9 @@ use self::editor_logic::{
|
||||
apply_connection_group_editor_name_key, clear_connection_editor_group_menu_draft,
|
||||
clear_connection_editor_runtime_state, commit_connection_editor_new_group,
|
||||
connection_editor_inline_panel_draft, connection_editor_window_open_or_pending,
|
||||
finish_connection_editor_save_state, focus_connection_editor_field,
|
||||
editor_field_seeds, finish_connection_editor_save_state, focus_connection_editor_field,
|
||||
insert_connection_editor_description_newline, set_connection_editor_advanced_tab,
|
||||
set_connection_editor_error, set_connection_editor_icon,
|
||||
set_connection_editor_error, set_connection_editor_field_text, set_connection_editor_icon,
|
||||
set_connection_editor_icon_auto_detect, set_connection_editor_kind,
|
||||
set_connection_editor_menu_value, set_connection_editor_password_source,
|
||||
set_connection_editor_telnet_tab, set_connection_group_editor_error,
|
||||
@@ -120,6 +120,14 @@ pub(in crate::features) struct ConnectionImportState {
|
||||
|
||||
pub(in crate::features) struct ConnectionEditorFeatureState {
|
||||
draft: Option<ConnectionEditorState>,
|
||||
/// One editable field per text input, built when the editor opens.
|
||||
///
|
||||
/// The draft above stays the source of truth for saving; these own the
|
||||
/// caret, selection and composition, and write back through their
|
||||
/// subscriptions. Keeping them out of `ConnectionEditorState` keeps that
|
||||
/// model a plain value the runtime can clone.
|
||||
fields: HashMap<ConnectionEditorField, Entity<TextField>>,
|
||||
field_subscriptions: Vec<Subscription>,
|
||||
window: Option<WindowHandle<ConnectionEditorWindow>>,
|
||||
window_open_pending: bool,
|
||||
focus: FocusHandle,
|
||||
@@ -129,6 +137,9 @@ pub(in crate::features) struct ConnectionEditorFeatureState {
|
||||
|
||||
pub(in crate::features) struct ConnectionGroupEditorFeatureState {
|
||||
draft: Option<ConnectionGroupEditorState>,
|
||||
/// The folder-name input, built with the draft it mirrors.
|
||||
field: Option<Entity<TextField>>,
|
||||
field_subscription: Option<Subscription>,
|
||||
focus: FocusHandle,
|
||||
}
|
||||
|
||||
@@ -205,6 +216,8 @@ impl ConnectionFeatureState {
|
||||
},
|
||||
editor: ConnectionEditorFeatureState {
|
||||
draft: None,
|
||||
fields: HashMap::new(),
|
||||
field_subscriptions: Vec::new(),
|
||||
window: None,
|
||||
window_open_pending: false,
|
||||
focus: focus.editor,
|
||||
@@ -213,6 +226,8 @@ impl ConnectionFeatureState {
|
||||
},
|
||||
group_editor: ConnectionGroupEditorFeatureState {
|
||||
draft: None,
|
||||
field: None,
|
||||
field_subscription: None,
|
||||
focus: focus.group_editor,
|
||||
},
|
||||
confirmations: ConnectionConfirmationState {
|
||||
@@ -239,6 +254,75 @@ impl ConnectionFeatureState {
|
||||
}
|
||||
}
|
||||
|
||||
/// The editor's fields, for handing to the render sections.
|
||||
pub fn editor_fields(&self) -> &HashMap<ConnectionEditorField, Entity<TextField>> {
|
||||
&self.editor.fields
|
||||
}
|
||||
|
||||
/// Build a field per input and wire each back into the draft.
|
||||
///
|
||||
/// Called when the editor opens, so the entities live exactly as long as the
|
||||
/// draft they mirror and never leak between edits.
|
||||
pub fn build_editor_fields(&mut self, cx: &mut Context<NyaTermApp>) {
|
||||
self.editor.fields.clear();
|
||||
self.editor.field_subscriptions.clear();
|
||||
let Some(draft) = self.editor.draft.as_ref() else {
|
||||
return;
|
||||
};
|
||||
for (field, value, masked) in editor_field_seeds(draft) {
|
||||
let entity = cx.new(|cx| TextField::new(cx, value).masked(masked));
|
||||
let subscription = cx.subscribe(&entity, move |app: &mut NyaTermApp, _, event, cx| {
|
||||
let TextFieldEvent::Changed(text) = event;
|
||||
app.apply_connection_editor_field_text(field, text.clone(), cx);
|
||||
});
|
||||
self.editor.fields.insert(field, entity);
|
||||
self.editor.field_subscriptions.push(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_editor_field_text(&mut self, field: ConnectionEditorField, text: String) {
|
||||
if let Some(draft) = self.editor.draft.as_mut() {
|
||||
set_connection_editor_field_text(draft, field, text);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn group_editor_field(&self) -> Option<Entity<TextField>> {
|
||||
self.group_editor.field.clone()
|
||||
}
|
||||
|
||||
pub fn build_group_editor_field(&mut self, cx: &mut Context<NyaTermApp>) {
|
||||
let Some(draft) = self.group_editor.draft.as_ref() else {
|
||||
self.group_editor.field = None;
|
||||
self.group_editor.field_subscription = None;
|
||||
return;
|
||||
};
|
||||
let entity = cx.new(|cx| TextField::new(cx, draft.name.clone()));
|
||||
let subscription = cx.subscribe(&entity, |app: &mut NyaTermApp, _, event, cx| {
|
||||
let TextFieldEvent::Changed(text) = event;
|
||||
app.connection_state.set_group_editor_name(text.clone());
|
||||
cx.notify();
|
||||
});
|
||||
self.group_editor.field = Some(entity);
|
||||
self.group_editor.field_subscription = Some(subscription);
|
||||
}
|
||||
|
||||
pub fn clear_group_editor_field(&mut self) {
|
||||
self.group_editor.field = None;
|
||||
self.group_editor.field_subscription = None;
|
||||
}
|
||||
|
||||
pub fn set_group_editor_name(&mut self, name: String) {
|
||||
if let Some(draft) = self.group_editor.draft.as_mut() {
|
||||
draft.name = name;
|
||||
draft.error = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_editor_fields(&mut self) {
|
||||
self.editor.fields.clear();
|
||||
self.editor.field_subscriptions.clear();
|
||||
}
|
||||
|
||||
pub fn finish_editor_save(&mut self, connection_id: String, group_id: Option<String>) {
|
||||
finish_connection_editor_save_state(
|
||||
&mut self.editor.draft,
|
||||
|
||||
@@ -511,3 +511,96 @@ pub(super) fn set_connection_group_editor_error(
|
||||
editor.error = Some(error);
|
||||
true
|
||||
}
|
||||
|
||||
/// Which draft strings become editable fields, and which are secrets.
|
||||
///
|
||||
/// Driven off the draft rather than a fixed list so a field that does not apply
|
||||
/// to the current kind is simply never built.
|
||||
pub(super) fn editor_field_seeds(
|
||||
draft: &ConnectionEditorState,
|
||||
) -> Vec<(ConnectionEditorField, String, bool)> {
|
||||
vec![
|
||||
(ConnectionEditorField::Name, draft.name.clone(), false),
|
||||
(
|
||||
ConnectionEditorField::Description,
|
||||
draft.description.clone(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::NewGroupName,
|
||||
draft.new_group_name.clone(),
|
||||
false,
|
||||
),
|
||||
(ConnectionEditorField::Host, draft.host.clone(), false),
|
||||
(ConnectionEditorField::Port, draft.port.clone(), false),
|
||||
(
|
||||
ConnectionEditorField::Username,
|
||||
draft.username.clone(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::Password,
|
||||
draft.password.clone(),
|
||||
true,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::ShellPath,
|
||||
draft.shell_path.clone(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::ShellArgs,
|
||||
draft.shell_args.clone(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::WorkingDir,
|
||||
draft.working_dir.clone(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::SerialPort,
|
||||
draft.serial_port.clone(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::BaudRate,
|
||||
draft.baud_rate.clone(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::PostLoginCommand,
|
||||
draft.post_login_command.clone(),
|
||||
false,
|
||||
),
|
||||
(
|
||||
ConnectionEditorField::PostLoginDelay,
|
||||
draft.post_login_delay_ms.clone(),
|
||||
false,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Write an edited field back into the draft.
|
||||
pub(super) fn set_connection_editor_field_text(
|
||||
draft: &mut ConnectionEditorState,
|
||||
field: ConnectionEditorField,
|
||||
text: String,
|
||||
) {
|
||||
match field {
|
||||
ConnectionEditorField::Name => draft.name = text,
|
||||
ConnectionEditorField::Description => draft.description = text,
|
||||
ConnectionEditorField::NewGroupName => draft.new_group_name = text,
|
||||
ConnectionEditorField::Host => draft.host = text,
|
||||
ConnectionEditorField::Port => draft.port = text,
|
||||
ConnectionEditorField::Username => draft.username = text,
|
||||
ConnectionEditorField::Password => draft.password = text,
|
||||
ConnectionEditorField::ShellPath => draft.shell_path = text,
|
||||
ConnectionEditorField::ShellArgs => draft.shell_args = text,
|
||||
ConnectionEditorField::WorkingDir => draft.working_dir = text,
|
||||
ConnectionEditorField::SerialPort => draft.serial_port = text,
|
||||
ConnectionEditorField::BaudRate => draft.baud_rate = text,
|
||||
ConnectionEditorField::PostLoginCommand => draft.post_login_command = text,
|
||||
ConnectionEditorField::PostLoginDelay => draft.post_login_delay_ms = text,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,18 @@ use gpui::{
|
||||
use crate::features::{NyaTermApp, small_button};
|
||||
use crate::models::{ConnectionEditorField, ConnectionEditorMenu, ConnectionEditorState};
|
||||
|
||||
use super::super::super::list::{ConnectionEditorChoice, connection_editor_select, editor_field};
|
||||
use super::super::super::list::{
|
||||
ConnectionEditorChoice, ConnectionEditorFields, connection_editor_select, editor_field,
|
||||
};
|
||||
|
||||
pub(super) fn connection_editor_local_section(
|
||||
palette: crate::theme::ThemePalette,
|
||||
editor: &ConnectionEditorState,
|
||||
_editor: &ConnectionEditorState,
|
||||
shell_label: &'static str,
|
||||
shell_options: Vec<ConnectionEditorChoice>,
|
||||
open_menu: Option<ConnectionEditorMenu>,
|
||||
language: &str,
|
||||
fields: &ConnectionEditorFields,
|
||||
cx: &mut Context<NyaTermApp>,
|
||||
) -> gpui::Div {
|
||||
let tr = |key: &'static str| crate::i18n::text(language, key);
|
||||
@@ -56,17 +59,10 @@ pub(super) fn connection_editor_local_section(
|
||||
)
|
||||
.child(div().min_w_0().flex_1().child(editor_field(
|
||||
palette,
|
||||
"connection-editor-shell",
|
||||
"",
|
||||
editor.shell_path.clone(),
|
||||
editor.focused_field == ConnectionEditorField::ShellPath,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(
|
||||
ConnectionEditorField::ShellPath,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
ConnectionEditorField::ShellPath,
|
||||
fields,
|
||||
cx,
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
@@ -96,13 +92,10 @@ pub(super) fn connection_editor_local_section(
|
||||
)
|
||||
.child(editor_field(
|
||||
palette,
|
||||
"connection-editor-args",
|
||||
tr("dialog.shellArgs"),
|
||||
editor.shell_args.clone(),
|
||||
editor.focused_field == ConnectionEditorField::ShellArgs,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(ConnectionEditorField::ShellArgs, window, cx);
|
||||
}),
|
||||
ConnectionEditorField::ShellArgs,
|
||||
fields,
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
@@ -111,17 +104,10 @@ pub(super) fn connection_editor_local_section(
|
||||
.gap_2()
|
||||
.child(div().min_w_0().flex_1().child(editor_field(
|
||||
palette,
|
||||
"connection-editor-cwd",
|
||||
tr("dialog.workingDir"),
|
||||
editor.working_dir.clone(),
|
||||
editor.focused_field == ConnectionEditorField::WorkingDir,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(
|
||||
ConnectionEditorField::WorkingDir,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
ConnectionEditorField::WorkingDir,
|
||||
fields,
|
||||
cx,
|
||||
)))
|
||||
.child(small_button(
|
||||
palette,
|
||||
|
||||
@@ -54,6 +54,7 @@ impl NyaTermApp {
|
||||
) -> AnyElement {
|
||||
let palette = self.theme_palette();
|
||||
let language = self.settings.language.clone();
|
||||
let fields = self.connection_state.editor_fields().clone();
|
||||
let title = if editor.id.is_some() {
|
||||
self.tr("dialog.editConnection")
|
||||
} else {
|
||||
@@ -373,15 +374,6 @@ impl NyaTermApp {
|
||||
selected: editor.shell_path == value,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let password_display = if editor.password.is_empty() {
|
||||
if editor.existing_password.is_some() {
|
||||
self.tr("dialog.passwordAlreadySet").to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
} else {
|
||||
"•".repeat(editor.password.chars().count().min(24))
|
||||
};
|
||||
let icon_key = editor.icon.as_deref();
|
||||
let icon_def = resolve_connection_icon(icon_key, editor.kind.label());
|
||||
let icon_picker_open = self.connection_state.editor.icon_picker_is_open();
|
||||
@@ -538,12 +530,10 @@ impl NyaTermApp {
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.overflow_hidden()
|
||||
// No blanket focus grab here: it existed to keep the old label-div
|
||||
// inputs "focused", and would now steal focus back from whichever
|
||||
// field the pointer just landed on, since click follows mouse-down.
|
||||
.track_focus(&editor_focus)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let editor_focus = this.connection_state.editor.focus_handle();
|
||||
window.focus(&editor_focus);
|
||||
cx.notify();
|
||||
}))
|
||||
.on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| {
|
||||
cx.stop_propagation();
|
||||
this.handle_connection_editor_key_down(event, window, cx);
|
||||
@@ -618,17 +608,10 @@ impl NyaTermApp {
|
||||
.child(icon_picker)
|
||||
.child(div().min_w(px(192.)).flex_1().child(editor_field(
|
||||
palette,
|
||||
"connection-editor-name",
|
||||
name_label,
|
||||
editor.name.clone(),
|
||||
editor.focused_field == ConnectionEditorField::Name,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(
|
||||
ConnectionEditorField::Name,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
ConnectionEditorField::Name,
|
||||
&fields,
|
||||
cx,
|
||||
)))
|
||||
.child(div().min_w(px(192.)).max_w(px(288.)).flex_1().child(
|
||||
connection_editor_group_select(
|
||||
@@ -649,7 +632,6 @@ impl NyaTermApp {
|
||||
this.child(connection_editor_ssh_section(
|
||||
palette,
|
||||
&editor,
|
||||
password_display.clone(),
|
||||
password_label.clone(),
|
||||
key_label.clone(),
|
||||
otp_label.clone(),
|
||||
@@ -664,6 +646,7 @@ impl NyaTermApp {
|
||||
backspace_options.clone(),
|
||||
active_menu,
|
||||
&language,
|
||||
&fields,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
@@ -675,6 +658,7 @@ impl NyaTermApp {
|
||||
shell_options,
|
||||
active_menu,
|
||||
&language,
|
||||
&fields,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
@@ -685,6 +669,7 @@ impl NyaTermApp {
|
||||
backspace_options.clone(),
|
||||
active_menu,
|
||||
&language,
|
||||
&fields,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
@@ -700,6 +685,7 @@ impl NyaTermApp {
|
||||
backspace_options,
|
||||
active_menu,
|
||||
&language,
|
||||
&fields,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
|
||||
@@ -7,7 +7,9 @@ use gpui::{
|
||||
use crate::features::NyaTermApp;
|
||||
use crate::models::{ConnectionEditorField, ConnectionEditorMenu, ConnectionEditorState};
|
||||
|
||||
use super::super::super::list::{ConnectionEditorChoice, connection_editor_select, editor_field};
|
||||
use super::super::super::list::{
|
||||
ConnectionEditorChoice, ConnectionEditorFields, connection_editor_select, editor_field,
|
||||
};
|
||||
|
||||
pub(super) fn connection_editor_serial_section(
|
||||
palette: crate::theme::ThemePalette,
|
||||
@@ -20,6 +22,7 @@ pub(super) fn connection_editor_serial_section(
|
||||
backspace_options: Vec<ConnectionEditorChoice>,
|
||||
open_menu: Option<ConnectionEditorMenu>,
|
||||
language: &str,
|
||||
fields: &ConnectionEditorFields,
|
||||
cx: &mut Context<NyaTermApp>,
|
||||
) -> gpui::Div {
|
||||
let tr = |key: &'static str| crate::i18n::text(language, key);
|
||||
@@ -66,17 +69,10 @@ pub(super) fn connection_editor_serial_section(
|
||||
.gap_1()
|
||||
.child(div().min_w_0().flex_1().child(editor_field(
|
||||
palette,
|
||||
"connection-editor-baud",
|
||||
tr("dialog.baudRate"),
|
||||
editor.baud_rate.clone(),
|
||||
editor.focused_field == ConnectionEditorField::BaudRate,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(
|
||||
ConnectionEditorField::BaudRate,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
ConnectionEditorField::BaudRate,
|
||||
fields,
|
||||
cx,
|
||||
)))
|
||||
.child(div().w(px(72.)).flex_none().child(connection_editor_select(
|
||||
palette,
|
||||
|
||||
@@ -15,7 +15,8 @@ use crate::models::{
|
||||
};
|
||||
|
||||
use super::super::super::list::{
|
||||
ConnectionEditorChoice, connection_editor_select, editor_field, toggle_chip,
|
||||
ConnectionEditorChoice, ConnectionEditorFields, connection_editor_select, editor_field,
|
||||
toggle_chip,
|
||||
};
|
||||
|
||||
fn ssh_segment_tab(
|
||||
@@ -99,7 +100,6 @@ fn ssh_advanced_content(
|
||||
pub(super) fn connection_editor_ssh_section(
|
||||
palette: crate::theme::ThemePalette,
|
||||
editor: &ConnectionEditorState,
|
||||
password_display: String,
|
||||
password_label: String,
|
||||
key_label: String,
|
||||
otp_label: String,
|
||||
@@ -114,6 +114,7 @@ pub(super) fn connection_editor_ssh_section(
|
||||
backspace_options: Vec<ConnectionEditorChoice>,
|
||||
open_menu: Option<ConnectionEditorMenu>,
|
||||
language: &str,
|
||||
fields: &ConnectionEditorFields,
|
||||
cx: &mut Context<NyaTermApp>,
|
||||
) -> gpui::Div {
|
||||
let tr = |key: &'static str| crate::i18n::text(language, key);
|
||||
@@ -295,34 +296,25 @@ pub(super) fn connection_editor_ssh_section(
|
||||
.gap_2()
|
||||
.child(editor_field(
|
||||
palette,
|
||||
"connection-editor-host",
|
||||
tr("dialog.host"),
|
||||
editor.host.clone(),
|
||||
editor.focused_field == ConnectionEditorField::Host,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(ConnectionEditorField::Host, window, cx);
|
||||
}),
|
||||
ConnectionEditorField::Host,
|
||||
fields,
|
||||
cx,
|
||||
))
|
||||
.child(editor_field(
|
||||
palette,
|
||||
"connection-editor-port",
|
||||
tr("dialog.port"),
|
||||
editor.port.clone(),
|
||||
editor.focused_field == ConnectionEditorField::Port,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(ConnectionEditorField::Port, window, cx);
|
||||
}),
|
||||
ConnectionEditorField::Port,
|
||||
fields,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(editor_field(
|
||||
palette,
|
||||
"connection-editor-username",
|
||||
tr("dialog.username"),
|
||||
editor.username.clone(),
|
||||
editor.focused_field == ConnectionEditorField::Username,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(ConnectionEditorField::Username, window, cx);
|
||||
}),
|
||||
ConnectionEditorField::Username,
|
||||
fields,
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
@@ -382,17 +374,10 @@ pub(super) fn connection_editor_ssh_section(
|
||||
|this| {
|
||||
this.child(editor_field(
|
||||
palette,
|
||||
"connection-editor-password",
|
||||
tr("dialog.password"),
|
||||
password_display.clone(),
|
||||
editor.focused_field == ConnectionEditorField::Password,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(
|
||||
ConnectionEditorField::Password,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
ConnectionEditorField::Password,
|
||||
fields,
|
||||
cx,
|
||||
))
|
||||
},
|
||||
)
|
||||
@@ -563,34 +548,18 @@ pub(super) fn connection_editor_ssh_section(
|
||||
.when(editor.post_login_enabled, |this| {
|
||||
this.child(editor_field(
|
||||
palette,
|
||||
"connection-editor-post-login-command",
|
||||
tr("dialog.postLoginCommandContent"),
|
||||
editor.post_login_command.clone(),
|
||||
editor.focused_field
|
||||
== ConnectionEditorField::PostLoginCommand,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(
|
||||
ConnectionEditorField::PostLoginCommand,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
ConnectionEditorField::PostLoginCommand,
|
||||
fields,
|
||||
cx,
|
||||
))
|
||||
.child(
|
||||
editor_field(
|
||||
palette,
|
||||
"connection-editor-post-login-delay",
|
||||
tr("dialog.postLoginDelay"),
|
||||
editor.post_login_delay_ms.clone(),
|
||||
editor.focused_field
|
||||
== ConnectionEditorField::PostLoginDelay,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(
|
||||
ConnectionEditorField::PostLoginDelay,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
ConnectionEditorField::PostLoginDelay,
|
||||
fields,
|
||||
cx,
|
||||
),
|
||||
)
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,9 @@ use crate::models::{
|
||||
ConnectionEditorField, ConnectionEditorMenu, ConnectionEditorState, ConnectionEditorTelnetTab,
|
||||
};
|
||||
|
||||
use super::super::super::list::{ConnectionEditorChoice, connection_editor_select, editor_field};
|
||||
use super::super::super::list::{
|
||||
ConnectionEditorChoice, ConnectionEditorFields, connection_editor_select, editor_field,
|
||||
};
|
||||
|
||||
fn telnet_segment_tab(
|
||||
palette: crate::theme::ThemePalette,
|
||||
@@ -132,6 +134,7 @@ pub(super) fn connection_editor_telnet_section(
|
||||
backspace_options: Vec<ConnectionEditorChoice>,
|
||||
open_menu: Option<ConnectionEditorMenu>,
|
||||
language: &str,
|
||||
fields: &ConnectionEditorFields,
|
||||
cx: &mut Context<NyaTermApp>,
|
||||
) -> gpui::Div {
|
||||
let tr = |key: &'static str| crate::i18n::text(language, key);
|
||||
@@ -194,23 +197,17 @@ pub(super) fn connection_editor_telnet_section(
|
||||
.gap_2()
|
||||
.child(editor_field(
|
||||
palette,
|
||||
"connection-editor-telnet-host",
|
||||
tr("dialog.host"),
|
||||
editor.host.clone(),
|
||||
editor.focused_field == ConnectionEditorField::Host,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(ConnectionEditorField::Host, window, cx);
|
||||
}),
|
||||
ConnectionEditorField::Host,
|
||||
fields,
|
||||
cx,
|
||||
))
|
||||
.child(editor_field(
|
||||
palette,
|
||||
"connection-editor-telnet-port",
|
||||
tr("dialog.port"),
|
||||
editor.port.clone(),
|
||||
editor.focused_field == ConnectionEditorField::Port,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.focus_connection_editor_field(ConnectionEditorField::Port, window, cx);
|
||||
}),
|
||||
ConnectionEditorField::Port,
|
||||
fields,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -15,8 +15,6 @@ use crate::models::{
|
||||
ConnectionGroupOpenConfirmState,
|
||||
};
|
||||
|
||||
use super::super::list::editor_field;
|
||||
|
||||
impl NyaTermApp {
|
||||
pub(in crate::features) fn connection_group_editor_panel(
|
||||
&mut self,
|
||||
@@ -52,18 +50,26 @@ impl NyaTermApp {
|
||||
.text_color(rgb(palette.text))
|
||||
.child(title),
|
||||
)
|
||||
.child(editor_field(
|
||||
palette,
|
||||
"connection-group-name",
|
||||
self.tr("savedConnections.folderName"),
|
||||
editor.name.clone(),
|
||||
true,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
let group_editor_focus = this.connection_state.group_editor.focus_handle();
|
||||
window.focus(&group_editor_focus);
|
||||
cx.notify();
|
||||
}),
|
||||
))
|
||||
.children(self.connection_state.group_editor_field().map(|field| {
|
||||
div()
|
||||
.h(px(36.))
|
||||
.px_3()
|
||||
.py_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_center()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(rgb(palette.primary))
|
||||
.bg(rgb(palette.input))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(rgb(palette.text_muted))
|
||||
.child(self.tr("savedConnections.folderName")),
|
||||
)
|
||||
.child(div().min_w_0().flex_1().text_xs().child(field))
|
||||
}))
|
||||
.when_some(editor.error.clone(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use gpui::{
|
||||
App, AppContext as _, ClickEvent, Context, FontWeight, IntoElement, SharedString, Window, div,
|
||||
App, AppContext as _, ClickEvent, Context, Entity, FontWeight, IntoElement, SharedString,
|
||||
Window, div,
|
||||
prelude::{
|
||||
FluentBuilder, InteractiveElement, ParentElement, StatefulInteractiveElement, Styled,
|
||||
},
|
||||
@@ -9,8 +10,9 @@ use gpui::{
|
||||
};
|
||||
use nyaterm_core::{Group, ProxyConfig, SavedConnection, natural_compare, truncate_preview};
|
||||
|
||||
use crate::features::{NyaTermApp, format_last_used_ms, transfer_input};
|
||||
use crate::models::{ConnectionEditorMenu, ConnectionSortMode};
|
||||
use crate::features::{NyaTermApp, format_last_used_ms};
|
||||
use crate::models::{ConnectionEditorField, ConnectionEditorMenu, ConnectionSortMode};
|
||||
use nyaterm_ui::TextField;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) enum ConnectionListRow {
|
||||
@@ -921,15 +923,62 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// The editor's text fields, resolved once per render.
|
||||
///
|
||||
/// Sections receive this rather than the app, because they are free functions
|
||||
/// that already take a `Context` and cannot borrow `NyaTermApp` again.
|
||||
pub(super) type ConnectionEditorFields = HashMap<ConnectionEditorField, Entity<TextField>>;
|
||||
|
||||
/// A labelled row wrapping one editable field.
|
||||
pub(super) fn editor_field(
|
||||
palette: crate::theme::ThemePalette,
|
||||
id: impl Into<String>,
|
||||
label: &'static str,
|
||||
value: String,
|
||||
active: bool,
|
||||
on_click: impl Fn(&gpui::ClickEvent, &mut gpui::Window, &mut gpui::App) + 'static,
|
||||
field: ConnectionEditorField,
|
||||
fields: &ConnectionEditorFields,
|
||||
cx: &App,
|
||||
) -> impl IntoElement {
|
||||
transfer_input(id, label, value, active, palette).on_click(on_click)
|
||||
// The whole labelled row is the hit target: the field itself is only one
|
||||
// text line tall, so clicking the label or the padding would otherwise miss.
|
||||
let entity = fields.get(&field);
|
||||
let handle = entity.map(|field| field.read(cx).focus_handle());
|
||||
let focused = entity.is_some_and(|field| field.read(cx).has_focus());
|
||||
let mut row = div()
|
||||
.h(px(36.))
|
||||
.px_3()
|
||||
.py_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_center()
|
||||
.gap_0()
|
||||
.rounded_sm()
|
||||
.border_1()
|
||||
.border_color(rgb(if focused {
|
||||
palette.primary
|
||||
} else {
|
||||
palette.border
|
||||
}))
|
||||
.bg(rgb(palette.input));
|
||||
if !label.is_empty() {
|
||||
row = row.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(rgb(palette.text_muted))
|
||||
.child(label),
|
||||
);
|
||||
}
|
||||
row.when_some(handle, |row, handle| {
|
||||
row.on_mouse_down(gpui::MouseButton::Left, move |_, window, _| {
|
||||
window.focus(&handle);
|
||||
})
|
||||
})
|
||||
.children(fields.get(&field).map(|field| {
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex_1()
|
||||
.text_xs()
|
||||
.text_color(rgb(palette.text))
|
||||
.child(field.clone())
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn icon_action_button(
|
||||
|
||||
@@ -79,7 +79,7 @@ pub(crate) enum ConnectionEditorTelnetTab {
|
||||
Compatibility,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum ConnectionEditorField {
|
||||
Name,
|
||||
NewGroupName,
|
||||
|
||||
@@ -54,6 +54,9 @@ pub struct TextField {
|
||||
/// Horizontal scroll, so a caret past the right edge stays visible.
|
||||
scroll_x: Pixels,
|
||||
caret_visible: bool,
|
||||
/// Focus as of the last render, so owners can style their own chrome with
|
||||
/// only an `&App` — focus itself is a window-scoped question.
|
||||
focused: bool,
|
||||
blink: Option<gpui::Task<()>>,
|
||||
selecting: bool,
|
||||
}
|
||||
@@ -68,6 +71,7 @@ impl TextField {
|
||||
marked: None,
|
||||
scroll_x: px(0.),
|
||||
caret_visible: true,
|
||||
focused: false,
|
||||
blink: None,
|
||||
selecting: false,
|
||||
}
|
||||
@@ -95,6 +99,10 @@ impl TextField {
|
||||
self.focus.clone()
|
||||
}
|
||||
|
||||
pub fn has_focus(&self) -> bool {
|
||||
self.focused
|
||||
}
|
||||
|
||||
/// Replace the buffer from the owner's state without emitting a change.
|
||||
pub fn set_content(&mut self, content: impl Into<String>, cx: &mut Context<Self>) {
|
||||
let content = content.into();
|
||||
@@ -289,6 +297,7 @@ fn buffer_offset(edit: &TextEdit, masked: bool, display: usize) -> usize {
|
||||
impl Render for TextField {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let focused = self.focus.is_focused(window);
|
||||
self.focused = focused;
|
||||
div()
|
||||
.id("text-field")
|
||||
.track_focus(&self.focus)
|
||||
|
||||
Reference in New Issue
Block a user