Merge upstream main into fix/issue-283-color-scheme-echo
This commit is contained in:
commit
785502c886
7 files changed
+218
-37
No files matched your search
@@ -136,6 +136,7 @@ impl ThemeAdvancedEditor {
|
||||
let ansi_pickers = (0..ANSI_COUNT)
|
||||
.map(|index| {
|
||||
new_color_picker(
|
||||
ThemeAdvancedField::Ansi(index),
|
||||
Some(definition.terminal.palette.get(index).unwrap_or([0, 0, 0])),
|
||||
ansi_inputs[index].clone(),
|
||||
window,
|
||||
@@ -145,6 +146,7 @@ impl ThemeAdvancedEditor {
|
||||
})
|
||||
.collect();
|
||||
let selection_background_picker = new_color_picker(
|
||||
ThemeAdvancedField::SelectionBackground,
|
||||
definition.terminal.selection_background,
|
||||
selection_background_input.clone(),
|
||||
window,
|
||||
@@ -152,6 +154,7 @@ impl ThemeAdvancedEditor {
|
||||
&mut picker_subscriptions,
|
||||
);
|
||||
let selection_foreground_picker = new_color_picker(
|
||||
ThemeAdvancedField::SelectionForeground,
|
||||
definition.terminal.selection_foreground,
|
||||
selection_foreground_input.clone(),
|
||||
window,
|
||||
@@ -159,6 +162,7 @@ impl ThemeAdvancedEditor {
|
||||
&mut picker_subscriptions,
|
||||
);
|
||||
let cursor_text_picker = new_color_picker(
|
||||
ThemeAdvancedField::CursorText,
|
||||
definition.terminal.cursor_text,
|
||||
cursor_text_input.clone(),
|
||||
window,
|
||||
@@ -211,6 +215,17 @@ impl ThemeAdvancedEditor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn picker(&self, field: ThemeAdvancedField) -> Option<&Entity<ColorPickerState>> {
|
||||
match field {
|
||||
ThemeAdvancedField::Ansi(index) => self.ansi_pickers.get(index),
|
||||
ThemeAdvancedField::SelectionBackground => Some(&self.selection_background_picker),
|
||||
ThemeAdvancedField::SelectionForeground => Some(&self.selection_foreground_picker),
|
||||
ThemeAdvancedField::CursorText => Some(&self.cursor_text_picker),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace all control values after a template/reset/import operation.
|
||||
/// Invalid input errors belong to the old draft and therefore disappear.
|
||||
pub(super) fn sync_from_definition(
|
||||
@@ -557,6 +572,7 @@ impl ThemeAdvancedEditor {
|
||||
}
|
||||
|
||||
fn new_color_picker(
|
||||
field: ThemeAdvancedField,
|
||||
color: Option<Rgb8>,
|
||||
input: Entity<InputState>,
|
||||
window: &mut Window,
|
||||
@@ -590,6 +606,7 @@ fn new_color_picker(
|
||||
input.update(cx, |state, cx| {
|
||||
state.set_value(format_hex_rgb(rgb), window, cx);
|
||||
});
|
||||
this.on_theme_advanced_input(field, window, cx);
|
||||
},
|
||||
);
|
||||
subscriptions.push(subscription);
|
||||
|
||||
@@ -938,7 +938,7 @@ impl SettingsPane {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_theme_advanced_input(
|
||||
pub(super) fn on_theme_advanced_input(
|
||||
&mut self,
|
||||
field: ThemeAdvancedField,
|
||||
window: &mut Window,
|
||||
@@ -964,6 +964,14 @@ impl SettingsPane {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn theme_advanced_picker(
|
||||
&self,
|
||||
field: ThemeAdvancedField,
|
||||
) -> Option<Entity<gpui_component::color_picker::ColorPickerState>> {
|
||||
self.theme_editor.as_ref()?.advanced_editor.as_ref()?.picker(field).cloned()
|
||||
}
|
||||
|
||||
fn request_close_theme_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.theme_editor.as_ref().is_some_and(|editor| editor.save_busy) {
|
||||
return;
|
||||
|
||||
@@ -21,6 +21,8 @@ impl SettingsPane {
|
||||
let foreground = theme_color(draft.terminal.foreground, 1.0);
|
||||
let accent = theme_color(resolved_ui.accent, 1.0);
|
||||
let cursor = theme_color(draft.terminal.cursor.unwrap_or(draft.terminal.foreground), 1.0);
|
||||
let cursor_text =
|
||||
theme_color(draft.terminal.cursor_text.unwrap_or(draft.terminal.background), 1.0);
|
||||
let selection_background = theme_color(
|
||||
draft
|
||||
.terminal
|
||||
@@ -233,7 +235,18 @@ impl SettingsPane {
|
||||
.gap(px(7.0))
|
||||
.items_center()
|
||||
.child(div().text_color(accent).child("❯"))
|
||||
.child(div().w(px(8.0)).h(px(font_size * line_height)).bg(cursor)),
|
||||
.child(
|
||||
div()
|
||||
.debug_selector(|| "theme-editor-preview-cursor".to_owned())
|
||||
.w(px(font_size * 0.78))
|
||||
.h(px(font_size * line_height))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cursor)
|
||||
.text_color(cursor_text)
|
||||
.child("A"),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
};
|
||||
|
||||
@@ -483,6 +483,28 @@ fn editor_template(pane: &Entity<SettingsPane>, cx: &mut VisualTestContext) -> T
|
||||
pane.read_with(cx, |pane, _| pane.theme_editor.as_ref().expect("theme editor state").template)
|
||||
}
|
||||
|
||||
fn emit_advanced_picker_color(
|
||||
pane: &Entity<SettingsPane>,
|
||||
field: super::theme_advanced::ThemeAdvancedField,
|
||||
color: [u8; 3],
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
let picker = pane
|
||||
.read_with(cx, |pane, _| pane.theme_advanced_picker(field).expect("advanced color picker"));
|
||||
picker.update(cx, |_, cx| {
|
||||
let [r, g, b] = color;
|
||||
let hsla: gpui::Hsla = gpui::Rgba {
|
||||
r: f32::from(r) / 255.0,
|
||||
g: f32::from(g) / 255.0,
|
||||
b: f32::from(b) / 255.0,
|
||||
a: 1.0,
|
||||
}
|
||||
.into();
|
||||
cx.emit(gpui_component::color_picker::ColorPickerEvent::Change(Some(hsla)));
|
||||
});
|
||||
draw(cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn theme_editor_opens_from_the_picker_with_common_fields_and_collapsed_advanced(
|
||||
cx: &mut TestAppContext,
|
||||
@@ -728,6 +750,92 @@ fn theme_editor_save_and_apply_persists_copy_and_reloads_its_foreground(cx: &mut
|
||||
assert_eq!(palette_foreground(&mut window), applied.color("terminal", "foreground").unwrap());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn advanced_selection_and_cursor_text_picker_colors_preview_persist_apply_and_reopen(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
use super::theme_advanced::ThemeAdvancedField;
|
||||
|
||||
let _fixture_guard = lock_theme_studio();
|
||||
let _settings_guard = SettingsBytesGuard::capture();
|
||||
std::fs::write(nebula_settings::settings_path(), TEST_SETTINGS)
|
||||
.expect("write isolated advanced color settings");
|
||||
let mut theme_cleanup = CustomThemeCleanup::default();
|
||||
let (pane, mut window) = open_settings(cx);
|
||||
let before_library_ids = custom_theme_ids();
|
||||
let saved_name = format!("Advanced colors native test {}", std::process::id());
|
||||
let selection_background = [0x1b, 0x2c, 0x3d];
|
||||
let selection_foreground = [0xe1, 0xd2, 0xc3];
|
||||
let cursor_text = [0x12, 0xeb, 0x77];
|
||||
|
||||
open_theme_editor(&mut window);
|
||||
edit_input("theme-editor-name", &saved_name, &mut window);
|
||||
click("theme-editor-advanced-toggle", &mut window);
|
||||
assert!(window.debug_bounds("theme-editor-preview-cursor").is_some());
|
||||
|
||||
emit_advanced_picker_color(
|
||||
&pane,
|
||||
ThemeAdvancedField::SelectionBackground,
|
||||
selection_background,
|
||||
&mut window,
|
||||
);
|
||||
emit_advanced_picker_color(
|
||||
&pane,
|
||||
ThemeAdvancedField::SelectionForeground,
|
||||
selection_foreground,
|
||||
&mut window,
|
||||
);
|
||||
emit_advanced_picker_color(&pane, ThemeAdvancedField::CursorText, cursor_text, &mut window);
|
||||
|
||||
let draft = editor_draft(&pane, &mut window);
|
||||
assert_eq!(draft.terminal.selection_background, Some(selection_background));
|
||||
assert_eq!(draft.terminal.selection_foreground, Some(selection_foreground));
|
||||
assert_eq!(draft.terminal.cursor_text, Some(cursor_text));
|
||||
|
||||
click("theme-editor-save-apply", &mut window);
|
||||
assert!(window.debug_bounds("theme-editor-dialog").is_none());
|
||||
|
||||
let after_documents = custom_theme_documents();
|
||||
for document in &after_documents {
|
||||
if document.id().is_some_and(|id| !before_library_ids.iter().any(|before| before == id))
|
||||
&& document.name().starts_with(&saved_name)
|
||||
{
|
||||
theme_cleanup.track(document);
|
||||
}
|
||||
}
|
||||
let applied_id = RuntimeSettings::load().custom_theme.expect("advanced color theme is active");
|
||||
let applied = after_documents
|
||||
.iter()
|
||||
.find(|document| document.id() == Some(applied_id.as_str()))
|
||||
.expect("advanced color theme remains in the library");
|
||||
let applied_definition = applied.definition().expect("saved advanced color definition");
|
||||
assert_eq!(applied_definition.terminal.selection_background, Some(selection_background));
|
||||
assert_eq!(applied_definition.terminal.selection_foreground, Some(selection_foreground));
|
||||
assert_eq!(applied_definition.terminal.cursor_text, Some(cursor_text));
|
||||
|
||||
window.read(|cx| {
|
||||
let palette = &cx.global::<crate::gpui_shell::config::Settings>().palette;
|
||||
let bytes = |color: gpui::Rgba| {
|
||||
[
|
||||
(color.r * 255.0).round() as u8,
|
||||
(color.g * 255.0).round() as u8,
|
||||
(color.b * 255.0).round() as u8,
|
||||
]
|
||||
};
|
||||
assert_eq!(bytes(palette.selection), selection_background);
|
||||
assert_eq!(palette.selection_foreground.map(bytes), Some(selection_foreground));
|
||||
assert_eq!(palette.cursor_text.map(bytes), Some(cursor_text));
|
||||
});
|
||||
|
||||
click("open-theme-picker", &mut window);
|
||||
assert!(window.debug_bounds("appearance-picker-dialog").is_some());
|
||||
click("customize-theme", &mut window);
|
||||
let reopened = editor_draft(&pane, &mut window);
|
||||
assert_eq!(reopened.terminal.selection_background, Some(selection_background));
|
||||
assert_eq!(reopened.terminal.selection_foreground, Some(selection_foreground));
|
||||
assert_eq!(reopened.terminal.cursor_text, Some(cursor_text));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn invalid_color_stays_dirty_and_save_rejects_after_a_later_valid_edit(cx: &mut TestAppContext) {
|
||||
let _fixture_guard = lock_theme_studio();
|
||||
|
||||
@@ -75,7 +75,7 @@ pub(super) fn virtual_key_of(key: &str) -> Option<u16> {
|
||||
/// Tab=0x09、Backspace=0x08):OpenConsole 1.22 的 VT 翻译层会丢弃 uChar=0
|
||||
/// 的 VK_ESCAPE,于是读字节流的那类应用(Claude Code)收不到 Esc。修饰键与
|
||||
/// 功能键保持 0,与真实键盘一致。逐条同旧壳 `control_char_fallback`。
|
||||
fn unicode_char_of(ks: &Keystroke, scan_code: u32) -> u16 {
|
||||
fn unicode_char_of(ks: &Keystroke, virtual_key: u16, scan_code: u32) -> u16 {
|
||||
// 平台已经判出文本的(含 Ctrl 变体)以它为准,与 WM_CHAR 语义一致。
|
||||
// `key_char` 若是 NUL,当作没文本:真实键盘的 Esc 不会写出 U+0000。
|
||||
if let Some(text) = ks.key_char.as_deref() {
|
||||
@@ -88,7 +88,8 @@ fn unicode_char_of(ks: &Keystroke, scan_code: u32) -> u16 {
|
||||
}
|
||||
match ks.key.as_str() {
|
||||
"escape" => 0x1b,
|
||||
"enter" => crate::platform::keyboard::enter_character(
|
||||
"enter" => crate::platform::keyboard::native_character(
|
||||
virtual_key,
|
||||
scan_code,
|
||||
ks.modifiers.shift,
|
||||
ks.modifiers.control,
|
||||
@@ -106,6 +107,24 @@ fn unicode_char_of(ks: &Keystroke, scan_code: u32) -> u16 {
|
||||
}
|
||||
},
|
||||
"space" => b' ' as u16,
|
||||
// GPUI 丢弃 Ctrl+字母的 C0 文本;Win32 记录必须恢复它,adb 等字节流
|
||||
// 应用才能收到 Ctrl+C。只补缺失文本,不把 AltGr 等组合合成为控制字符。
|
||||
_ if ks.key_char.is_none()
|
||||
&& ks.modifiers.control
|
||||
&& !ks.modifiers.alt
|
||||
&& !ks.modifiers.platform
|
||||
&& !ks.modifiers.function
|
||||
&& matches!(virtual_key, 0x41..=0x5a) =>
|
||||
{
|
||||
let character = crate::platform::keyboard::native_character(
|
||||
virtual_key,
|
||||
scan_code,
|
||||
ks.modifiers.shift,
|
||||
ks.modifiers.control,
|
||||
ks.modifiers.alt,
|
||||
);
|
||||
if character <= 0x1f { character } else { 0 }
|
||||
},
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -140,7 +159,7 @@ pub(super) fn win32_input_record(ks: &Keystroke, key_down: bool) -> Option<Vec<u
|
||||
"\x1b[{};{};{};{key_down};{};1_",
|
||||
vk,
|
||||
scan_code,
|
||||
unicode_char_of(ks, scan_code),
|
||||
unicode_char_of(ks, vk, scan_code),
|
||||
control_key_state
|
||||
)
|
||||
.into_bytes(),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use super::*;
|
||||
use crate::i18n::Message;
|
||||
use gpui_component::dialog::CancelDialog;
|
||||
|
||||
impl NebulaWorkspace {
|
||||
pub(super) fn remote_transfer_snapshot(&self) -> Option<SftpSnapshot> {
|
||||
@@ -296,20 +297,22 @@ impl NebulaWorkspace {
|
||||
let overwrite_workspace = workspace.clone();
|
||||
let overwrite_pending = pending.clone();
|
||||
let overwrite_target = target.clone();
|
||||
// DialogClose 的固定 ID 会让同级包装器共享点击状态;直接在按钮上
|
||||
// 派发关闭动作,也让鼠标和键盘激活使用同一条路径。
|
||||
let footer = DialogFooter::new()
|
||||
.child(
|
||||
DialogClose::new().child(
|
||||
Button::new("sftp-conflict-cancel")
|
||||
.label(language.text(Message::TransferCancel)),
|
||||
),
|
||||
Button::new("sftp-conflict-cancel")
|
||||
.label(language.text(Message::TransferCancel))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(CancelDialog), cx);
|
||||
}),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
DialogClose::new().child(
|
||||
Button::new("sftp-conflict-skip")
|
||||
.label(language.text(Message::TransferSkip))
|
||||
.on_click(move |_, _, cx| {
|
||||
let Some(workspace) = skip_workspace.upgrade() else { return };
|
||||
Button::new("sftp-conflict-skip")
|
||||
.label(language.text(Message::TransferSkip))
|
||||
.on_click(move |_, window, cx| {
|
||||
if let Some(workspace) = skip_workspace.upgrade() {
|
||||
let _ = workspace.update(cx, |workspace, cx| {
|
||||
workspace.start_remote_transfer(
|
||||
skip_pending.clone(),
|
||||
@@ -318,15 +321,15 @@ impl NebulaWorkspace {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}),
|
||||
),
|
||||
}
|
||||
window.dispatch_action(Box::new(CancelDialog), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
DialogClose::new().child(
|
||||
Button::new("sftp-conflict-keep-both")
|
||||
.label(language.text(Message::TransferKeepBoth))
|
||||
.on_click(move |_, _, cx| {
|
||||
let Some(workspace) = keep_workspace.upgrade() else { return };
|
||||
Button::new("sftp-conflict-keep-both")
|
||||
.label(language.text(Message::TransferKeepBoth))
|
||||
.on_click(move |_, window, cx| {
|
||||
if let Some(workspace) = keep_workspace.upgrade() {
|
||||
let _ = workspace.update(cx, |workspace, cx| {
|
||||
workspace.start_remote_transfer(
|
||||
keep_pending.clone(),
|
||||
@@ -335,17 +338,17 @@ impl NebulaWorkspace {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}),
|
||||
),
|
||||
}
|
||||
window.dispatch_action(Box::new(CancelDialog), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
DialogClose::new().child(
|
||||
Button::new("sftp-conflict-overwrite")
|
||||
.label(language.text(Message::TransferOverwrite))
|
||||
.danger()
|
||||
.disabled(!overwrite_allowed)
|
||||
.on_click(move |_, _, cx| {
|
||||
let Some(workspace) = overwrite_workspace.upgrade() else { return };
|
||||
Button::new("sftp-conflict-overwrite")
|
||||
.label(language.text(Message::TransferOverwrite))
|
||||
.danger()
|
||||
.disabled(!overwrite_allowed)
|
||||
.on_click(move |_, window, cx| {
|
||||
if let Some(workspace) = overwrite_workspace.upgrade() {
|
||||
let _ = workspace.update(cx, |workspace, cx| {
|
||||
workspace.start_remote_transfer(
|
||||
overwrite_pending.clone(),
|
||||
@@ -354,13 +357,20 @@ impl NebulaWorkspace {
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}),
|
||||
),
|
||||
}
|
||||
window.dispatch_action(Box::new(CancelDialog), cx);
|
||||
}),
|
||||
);
|
||||
|
||||
center_modal_dialog(dialog, window, 220.0)
|
||||
.close_button(false)
|
||||
.overlay_closable(true)
|
||||
// 此处没有默认传输策略。Enter 应交给聚焦按钮的按下/抬起处理,
|
||||
// 不能由 Dialog 的默认确认动作直接关闭而跳过策略选择。
|
||||
.on_ok(|_, _, cx| {
|
||||
cx.propagate();
|
||||
false
|
||||
})
|
||||
.title(
|
||||
div()
|
||||
.text_lg()
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
//! Native character data discarded by the GPUI keyboard adapter.
|
||||
|
||||
/// Recover Return's native character without changing keyboard composition state.
|
||||
/// GPUI filters control text out of `key_char`, including Ctrl+Enter's LF.
|
||||
pub(crate) fn enter_character(scan_code: u32, shift: bool, control: bool, alt: bool) -> u16 {
|
||||
/// Recover native character data without changing keyboard composition state.
|
||||
/// GPUI filters control text out of `key_char`, including Ctrl+C's ETX.
|
||||
pub(crate) fn native_character(
|
||||
virtual_key: u16,
|
||||
scan_code: u32,
|
||||
shift: bool,
|
||||
control: bool,
|
||||
alt: bool,
|
||||
) -> u16 {
|
||||
use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
|
||||
ToUnicode, VK_CONTROL, VK_MENU, VK_RETURN, VK_SHIFT,
|
||||
ToUnicode, VK_CONTROL, VK_MENU, VK_SHIFT,
|
||||
};
|
||||
|
||||
let mut state = [0_u8; 256];
|
||||
@@ -16,7 +22,7 @@ pub(crate) fn enter_character(scan_code: u32, shift: bool, control: bool, alt: b
|
||||
// prevents dead-key state mutation; 0x1 selects menu translation semantics.
|
||||
let count = unsafe {
|
||||
ToUnicode(
|
||||
u32::from(VK_RETURN),
|
||||
u32::from(virtual_key),
|
||||
scan_code,
|
||||
state.as_ptr(),
|
||||
text.as_mut_ptr(),
|
||||
|
||||
Reference in new issue
Block a user