i18n, a11y, IME · palette copy reads i18n with builtin en/zh; dialog and palette Enter run on the field's key-down action, so an IME-kept Enter never submits; Button, IconButton, TextInput, Tabs, EditorTabs, StatusBarItem and palette rows name their role and label
This commit is contained in:
1 parent
8fd7b8ab87
commit
4580e3b075
15 files changed
+257
-75
No files matched your search
@@ -1,6 +1,7 @@
|
||||
use gpui::{
|
||||
App, ClickEvent, ElementId, FocusHandle, FontWeight, Hsla, IntoElement, MouseButton,
|
||||
RenderOnce, SharedString, Window, div, prelude::*, transparent_black,
|
||||
RenderOnce, Role, SharedString, StatefulInteractiveElement, Window, div, prelude::*,
|
||||
transparent_black,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -232,6 +233,7 @@ impl RenderOnce for Button {
|
||||
let platform = cx.theme().platform;
|
||||
let hint = self.shortcut.map(|source| shortcut_text(&source, platform));
|
||||
let spinner = (self.id.clone(), "spinner");
|
||||
let name = self.label.clone();
|
||||
let parts = div()
|
||||
.flex()
|
||||
.items_center()
|
||||
@@ -251,6 +253,8 @@ impl RenderOnce for Button {
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.role(Role::Button)
|
||||
.when(!name.is_empty(), |el| el.aria_label(name))
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
App, ClickEvent, ElementId, IntoElement, MouseButton, RenderOnce, SharedString, Window, div,
|
||||
prelude::*,
|
||||
App, ClickEvent, ElementId, IntoElement, MouseButton, RenderOnce, Role, SharedString, Window,
|
||||
div, prelude::*,
|
||||
};
|
||||
|
||||
use super::button::{ButtonVariant, ClickHandler, label_size, tone};
|
||||
@@ -70,9 +70,15 @@ impl RenderOnce for IconButton {
|
||||
let tone = tone(self.variant, &theme.colors);
|
||||
let (_, icon_size) = label_size(self.size);
|
||||
let side = theme.control_height(self.size);
|
||||
let name: SharedString = self
|
||||
.tooltip
|
||||
.clone()
|
||||
.unwrap_or_else(|| self.icon.name().into());
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.role(Role::Button)
|
||||
.aria_label(name)
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
|
||||
@@ -6,6 +6,7 @@ use gpui::{
|
||||
|
||||
use crate::{
|
||||
buttons::SegmentedControl,
|
||||
i18n,
|
||||
lists::{Tree, TreeNode},
|
||||
navigation::{Group, Palette, Row, fuzzy, marked, query_field},
|
||||
primitives::IconName,
|
||||
@@ -258,7 +259,12 @@ impl GoToSymbol {
|
||||
|
||||
impl RenderOnce for GoToSymbol {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = query_field(&self.id, "Go to a symbol", window, cx);
|
||||
let input = query_field(
|
||||
&self.id,
|
||||
i18n::text(cx, "palette.symbol.placeholder", &[]),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let query = input.read(cx).text().trim().to_string();
|
||||
let colors = cx.theme().colors.clone();
|
||||
let mut found: Vec<(i32, Row)> = flattened(&self.symbols)
|
||||
@@ -278,6 +284,7 @@ impl RenderOnce for GoToSymbol {
|
||||
});
|
||||
let row = Row {
|
||||
value: symbol.line.to_string().into(),
|
||||
name: symbol.name.clone(),
|
||||
icon: Some(symbol.kind.icon()),
|
||||
label: marked(symbol.name.clone(), hits, cx),
|
||||
detail,
|
||||
@@ -310,7 +317,7 @@ impl RenderOnce for GoToSymbol {
|
||||
rows: found.into_iter().map(|(_, row)| row).collect(),
|
||||
}],
|
||||
start: 0,
|
||||
empty: "No matching symbols".into(),
|
||||
empty: i18n::text(cx, "palette.symbol.empty", &[]),
|
||||
on_pick,
|
||||
on_close: self.on_close,
|
||||
}
|
||||
|
||||
+21
-14
@@ -9,6 +9,7 @@ use gpui::{
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
forms::Checkbox,
|
||||
i18n,
|
||||
navigation::{Group, Palette, Row, fuzzy, marked, query_field},
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
@@ -83,7 +84,7 @@ impl RenderOnce for RecentProjects {
|
||||
);
|
||||
let button = |key: &'static str,
|
||||
icon: IconName,
|
||||
words: &'static str,
|
||||
words: SharedString,
|
||||
action: Option<OnIndex>| {
|
||||
IconButton::new((self.id.clone(), format!("{key}-{ix}")), icon)
|
||||
.variant(ButtonVariant::Ghost)
|
||||
@@ -96,6 +97,15 @@ impl RenderOnce for RecentProjects {
|
||||
})
|
||||
})
|
||||
};
|
||||
let (pin_icon, pin_tip) = if project.pinned {
|
||||
(
|
||||
IconName::PinOff,
|
||||
i18n::text(cx, "palette.project.unpin", &[]),
|
||||
)
|
||||
} else {
|
||||
(IconName::Pin, i18n::text(cx, "palette.project.pin", &[]))
|
||||
};
|
||||
let remove_tip = i18n::text(cx, "palette.project.remove", &[]);
|
||||
div()
|
||||
.id((self.id.clone(), format!("project-{ix}")))
|
||||
.flex()
|
||||
@@ -157,17 +167,8 @@ impl RenderOnce for RecentProjects {
|
||||
.text_color(colors.fg_subtle)
|
||||
.child(project.opened.clone()),
|
||||
)
|
||||
.child(button(
|
||||
"pin",
|
||||
if project.pinned {
|
||||
IconName::PinOff
|
||||
} else {
|
||||
IconName::Pin
|
||||
},
|
||||
if project.pinned { "Unpin" } else { "Pin" },
|
||||
pin,
|
||||
))
|
||||
.child(button("remove", IconName::X, "Remove from recent", remove))
|
||||
.child(button("pin", pin_icon, pin_tip, pin))
|
||||
.child(button("remove", IconName::X, remove_tip, remove))
|
||||
});
|
||||
div()
|
||||
.flex()
|
||||
@@ -212,7 +213,12 @@ impl ProjectSwitcher {
|
||||
|
||||
impl RenderOnce for ProjectSwitcher {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = query_field(&self.id, "Switch to a project", window, cx);
|
||||
let input = query_field(
|
||||
&self.id,
|
||||
i18n::text(cx, "palette.project.placeholder", &[]),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let query = input.read(cx).text().trim().to_string();
|
||||
let colors = cx.theme().colors.clone();
|
||||
let mut found: Vec<(i32, Row)> = ordered(&self.projects)
|
||||
@@ -227,6 +233,7 @@ impl RenderOnce for ProjectSwitcher {
|
||||
score,
|
||||
Row {
|
||||
value: ix.to_string().into(),
|
||||
name: project.name.clone(),
|
||||
icon: Some(if project.pinned {
|
||||
IconName::Pin
|
||||
} else {
|
||||
@@ -271,7 +278,7 @@ impl RenderOnce for ProjectSwitcher {
|
||||
rows: found.into_iter().map(|(_, row)| row).collect(),
|
||||
}],
|
||||
start: 0,
|
||||
empty: "No matching projects".into(),
|
||||
empty: i18n::text(cx, "palette.project.empty", &[]),
|
||||
on_pick,
|
||||
on_close: self.on_close,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
App, ClipboardItem, Context, CursorStyle, InteractiveElement, IntoElement, KeyBinding,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Render, Styled, Window, actions,
|
||||
div,
|
||||
MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Render, Role,
|
||||
StatefulInteractiveElement, Styled, Window, actions, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -359,6 +359,11 @@ impl Render for TextInput {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("ely-text-input")
|
||||
.role(Role::TextInput)
|
||||
.when(!self.placeholder.is_empty(), |el| {
|
||||
el.aria_label(self.placeholder.clone())
|
||||
})
|
||||
.when(!self.masked, |el| el.aria_value(self.text.clone()))
|
||||
.key_context(CONTEXT)
|
||||
.track_focus(&self.focus)
|
||||
.cursor(if self.disabled {
|
||||
|
||||
+16
-11
@@ -7,6 +7,7 @@ use gpui::{
|
||||
|
||||
use crate::{
|
||||
buttons::{ButtonVariant, IconButton},
|
||||
i18n,
|
||||
navigation::{Group, Palette, Row, fuzzy, marked, query_field},
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Radius, TextSize},
|
||||
@@ -87,7 +88,7 @@ impl RenderOnce for BranchSelector {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = query_field(
|
||||
&self.id,
|
||||
"Switch to a branch, or name a new one",
|
||||
i18n::text(cx, "palette.branch.placeholder", &[]),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -104,6 +105,7 @@ impl RenderOnce for BranchSelector {
|
||||
score,
|
||||
Row {
|
||||
value: branch.name.clone(),
|
||||
name: branch.name.clone(),
|
||||
icon: Some(if branch.current {
|
||||
IconName::Check
|
||||
} else {
|
||||
@@ -125,7 +127,7 @@ impl RenderOnce for BranchSelector {
|
||||
},
|
||||
))
|
||||
};
|
||||
let group = |title: &'static str, remote: bool, cx: &App| {
|
||||
let group = |title: SharedString, remote: bool, cx: &App| {
|
||||
let mut rows: Vec<(i32, Row)> = self
|
||||
.branches
|
||||
.iter()
|
||||
@@ -136,26 +138,29 @@ impl RenderOnce for BranchSelector {
|
||||
rows.sort_by(|(a, _), (b, _)| b.cmp(a));
|
||||
}
|
||||
Group {
|
||||
title: Some(title.into()),
|
||||
title: Some(title),
|
||||
rows: rows.into_iter().map(|(_, row)| row).collect(),
|
||||
}
|
||||
};
|
||||
let mut groups = vec![group("Local", false, cx), group("Remote", true, cx)];
|
||||
let mut groups = vec![
|
||||
group(i18n::text(cx, "palette.branch.local", &[]), false, cx),
|
||||
group(i18n::text(cx, "palette.branch.remote", &[]), true, cx),
|
||||
];
|
||||
let named = self
|
||||
.branches
|
||||
.iter()
|
||||
.any(|branch| branch.name.as_ref() == query);
|
||||
if !query.is_empty() && !named {
|
||||
let create = i18n::text(cx, "palette.branch.create", &[("query", &query)]);
|
||||
groups.insert(
|
||||
0,
|
||||
Group {
|
||||
title: None,
|
||||
rows: vec![Row {
|
||||
value: format!("{CREATE}{query}").into(),
|
||||
name: create.clone(),
|
||||
icon: Some(IconName::Plus),
|
||||
label: div()
|
||||
.child(format!("Create branch “{query}”"))
|
||||
.into_any_element(),
|
||||
label: div().child(create).into_any_element(),
|
||||
detail: None,
|
||||
end: None,
|
||||
}],
|
||||
@@ -184,7 +189,7 @@ impl RenderOnce for BranchSelector {
|
||||
input,
|
||||
groups,
|
||||
start: 0,
|
||||
empty: "No branch fits".into(),
|
||||
empty: i18n::text(cx, "palette.branch.empty", &[]),
|
||||
on_pick: Some(on_pick),
|
||||
on_close: self.on_close,
|
||||
}
|
||||
@@ -242,7 +247,7 @@ pub(super) fn listed(
|
||||
pub(super) fn action(
|
||||
id: ElementId,
|
||||
icon: IconName,
|
||||
words: &'static str,
|
||||
words: impl Into<SharedString>,
|
||||
run: Option<Run>,
|
||||
) -> AnyElement {
|
||||
IconButton::new(id, icon)
|
||||
@@ -320,13 +325,13 @@ impl RenderOnce for BranchList {
|
||||
action(
|
||||
(self.id.clone(), format!("switch-{}", branch.name)).into(),
|
||||
IconName::ArrowRight,
|
||||
"Switch to it",
|
||||
i18n::text(cx, "palette.branch.switch", &[]),
|
||||
bind(&self.on_switch),
|
||||
),
|
||||
action(
|
||||
(self.id.clone(), format!("delete-{}", branch.name)).into(),
|
||||
IconName::Trash2,
|
||||
"Delete",
|
||||
i18n::text(cx, "palette.branch.delete", &[]),
|
||||
bind(&self.on_delete),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Component copy in English and Chinese; `text` reads it when the app's catalogs lack the key.
|
||||
|
||||
/// English, the canonical set; zh-CN holds the same keys, a test keeps parity.
|
||||
const EN: &[(&str, &str)] = &[
|
||||
("palette.command.placeholder", "Type a command"),
|
||||
("palette.command.empty", "No matching commands"),
|
||||
("palette.recent", "Recent"),
|
||||
("palette.open.placeholder", "Go to file"),
|
||||
("palette.open.idle", "Type part of a file name"),
|
||||
("palette.open.empty", "No matching files"),
|
||||
("palette.switch.placeholder", "Switch to"),
|
||||
("palette.switch.empty", "Nothing open by that name"),
|
||||
("palette.search.placeholder", "Search everything"),
|
||||
("palette.search.idle", "Type to search"),
|
||||
("palette.search.empty", "Nothing found for “{query}”"),
|
||||
("palette.symbol.placeholder", "Go to a symbol"),
|
||||
("palette.symbol.empty", "No matching symbols"),
|
||||
("palette.project.placeholder", "Switch to a project"),
|
||||
("palette.project.empty", "No matching projects"),
|
||||
("palette.project.pin", "Pin"),
|
||||
("palette.project.unpin", "Unpin"),
|
||||
("palette.project.remove", "Remove from recent"),
|
||||
(
|
||||
"palette.branch.placeholder",
|
||||
"Switch to a branch, or name a new one",
|
||||
),
|
||||
("palette.branch.local", "Local"),
|
||||
("palette.branch.remote", "Remote"),
|
||||
("palette.branch.create", "Create branch “{query}”"),
|
||||
("palette.branch.empty", "No branch fits"),
|
||||
("palette.branch.switch", "Switch to it"),
|
||||
("palette.branch.delete", "Delete"),
|
||||
("palette.history.placeholder", "Search past commands"),
|
||||
("palette.history.empty", "No past command fits"),
|
||||
];
|
||||
|
||||
const ZH_CN: &[(&str, &str)] = &[
|
||||
("palette.command.placeholder", "输入命令"),
|
||||
("palette.command.empty", "没有匹配的命令"),
|
||||
("palette.recent", "最近"),
|
||||
("palette.open.placeholder", "转到文件"),
|
||||
("palette.open.idle", "输入文件名的一部分"),
|
||||
("palette.open.empty", "没有匹配的文件"),
|
||||
("palette.switch.placeholder", "切换到"),
|
||||
("palette.switch.empty", "没有以此名字打开的"),
|
||||
("palette.search.placeholder", "搜索全部"),
|
||||
("palette.search.idle", "输入以搜索"),
|
||||
("palette.search.empty", "没有找到“{query}”"),
|
||||
("palette.symbol.placeholder", "转到符号"),
|
||||
("palette.symbol.empty", "没有匹配的符号"),
|
||||
("palette.project.placeholder", "切换项目"),
|
||||
("palette.project.empty", "没有匹配的项目"),
|
||||
("palette.project.pin", "置顶"),
|
||||
("palette.project.unpin", "取消置顶"),
|
||||
("palette.project.remove", "从最近中移除"),
|
||||
("palette.branch.placeholder", "切换分支,或输入新分支名"),
|
||||
("palette.branch.local", "本地"),
|
||||
("palette.branch.remote", "远程"),
|
||||
("palette.branch.create", "创建分支“{query}”"),
|
||||
("palette.branch.empty", "没有匹配的分支"),
|
||||
("palette.branch.switch", "切换过去"),
|
||||
("palette.branch.delete", "删除"),
|
||||
("palette.history.placeholder", "搜索历史命令"),
|
||||
("palette.history.empty", "没有匹配的历史命令"),
|
||||
];
|
||||
|
||||
/// Ely's own message for `key` in `locale`, English for a locale Ely does not ship; fails on an unknown key.
|
||||
pub(crate) fn builtin(locale: &str, key: &str) -> &'static str {
|
||||
let table = if locale == "zh-CN" { ZH_CN } else { EN };
|
||||
table
|
||||
.iter()
|
||||
.find_map(|(k, v)| (*k == key).then_some(*v))
|
||||
.or_else(|| EN.iter().find_map(|(k, v)| (*k == key).then_some(*v)))
|
||||
.unwrap_or_else(|| panic!("no builtin message {key:?}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{EN, ZH_CN};
|
||||
|
||||
#[test]
|
||||
fn zh_cn_holds_every_english_key() {
|
||||
let zh: std::collections::HashSet<_> = ZH_CN.iter().map(|(key, _)| key).collect();
|
||||
assert_eq!(EN.len(), ZH_CN.len());
|
||||
assert!(EN.iter().all(|(key, _)| zh.contains(key)));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod builtin;
|
||||
mod locale;
|
||||
mod skip;
|
||||
#[cfg(test)]
|
||||
@@ -73,6 +74,14 @@ impl I18n {
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
/// The locale's own message for `key`, nothing when its catalog lacks it.
|
||||
pub(crate) fn find(&self, key: &str) -> Option<&'static str> {
|
||||
self.catalogs
|
||||
.iter()
|
||||
.find(|(tag, _)| *tag == self.locale.tag)
|
||||
.and_then(|(_, catalog)| catalog.get(key).copied())
|
||||
}
|
||||
|
||||
/// The shown locale's message for `key`, each `{name}` filled from `args`; fails on a missing key or argument. gpui sets no paragraph direction, so a right-to-left message leads with a right-to-left mark: CoreText would lay out one that starts with Latin letters or a signed number left to right.
|
||||
pub fn text(&self, key: &str, args: &[(&str, &str)]) -> SharedString {
|
||||
let (_, catalog) = self
|
||||
@@ -92,6 +101,24 @@ impl I18n {
|
||||
}
|
||||
}
|
||||
|
||||
/// A component's message: the app's catalog first, Ely's builtin copy after. Without an `I18n` global it reads the English builtin.
|
||||
pub fn text(cx: &App, key: &str, args: &[(&str, &str)]) -> SharedString {
|
||||
let (message, direction) = if cx.has_global::<I18n>() {
|
||||
let i18n = cx.global::<I18n>();
|
||||
let message = i18n
|
||||
.find(key)
|
||||
.unwrap_or_else(|| builtin::builtin(i18n.locale().tag, key));
|
||||
(message, i18n.locale().direction)
|
||||
} else {
|
||||
(builtin::builtin("en-US", key), Direction::Ltr)
|
||||
};
|
||||
let text = fill(message, args).unwrap_or_else(|error| panic!("{key:?}: {error}"));
|
||||
match direction {
|
||||
Direction::Ltr => text.into(),
|
||||
Direction::Rtl => format!("\u{200f}{text}").into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `message` with each `{name}` replaced by its argument.
|
||||
fn fill(message: &str, args: &[(&str, &str)]) -> Result<String, String> {
|
||||
let mut out = String::with_capacity(message.len());
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, ElementId, EntityId, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, RenderOnce, ScrollHandle, SharedString, StatefulInteractiveElement,
|
||||
Styled, Window, canvas, div, prelude::*,
|
||||
ParentElement, Pixels, RenderOnce, Role, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, canvas, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::TabOverflowMenu;
|
||||
@@ -246,6 +246,9 @@ impl RenderOnce for EditorTabs {
|
||||
let (title, icon, edge) = (tab.title.clone(), tab.icon, colors.focus);
|
||||
div()
|
||||
.id(tab.id.clone())
|
||||
.role(Role::Tab)
|
||||
.aria_selected(on)
|
||||
.aria_label(tab.title.clone())
|
||||
.group(group)
|
||||
.relative()
|
||||
.flex()
|
||||
|
||||
@@ -5,6 +5,7 @@ use gpui::{App, ElementId, IntoElement, RenderOnce, SharedString, Window};
|
||||
use super::{Fit, Group, Palette, Row, fuzzy, marked, query_field};
|
||||
use crate::{
|
||||
forms::{Choice, OnValue, Run},
|
||||
i18n,
|
||||
primitives::IconName,
|
||||
typography::{Highlight, KbdCombo},
|
||||
};
|
||||
@@ -60,6 +61,7 @@ impl Command {
|
||||
fn row(&self, hits: Vec<Range<usize>>, cx: &App) -> Row {
|
||||
Row {
|
||||
value: self.value.clone(),
|
||||
name: self.label.clone(),
|
||||
icon: self.icon,
|
||||
label: marked(self.label.clone(), hits, cx),
|
||||
detail: None,
|
||||
@@ -123,7 +125,8 @@ impl CommandPalette {
|
||||
|
||||
impl RenderOnce for CommandPalette {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = query_field(&self.id, "Type a command", window, cx);
|
||||
let placeholder = i18n::text(cx, "palette.command.placeholder", &[]);
|
||||
let input = query_field(&self.id, placeholder, window, cx);
|
||||
let query = input.read(cx).text().trim().to_string();
|
||||
let groups = if query.is_empty() {
|
||||
let find = |value: &SharedString| {
|
||||
@@ -134,7 +137,7 @@ impl RenderOnce for CommandPalette {
|
||||
.unwrap_or_else(|| panic!("recent command {value} is not in the palette"))
|
||||
};
|
||||
let recent = Group {
|
||||
title: Some("Recent".into()),
|
||||
title: Some(i18n::text(cx, "palette.recent", &[])),
|
||||
rows: self
|
||||
.recent
|
||||
.iter()
|
||||
@@ -182,7 +185,7 @@ impl RenderOnce for CommandPalette {
|
||||
input,
|
||||
groups,
|
||||
start: 0,
|
||||
empty: "No matching commands".into(),
|
||||
empty: i18n::text(cx, "palette.command.empty", &[]),
|
||||
on_pick: self.on_run,
|
||||
on_close: self.on_close,
|
||||
}
|
||||
@@ -246,6 +249,7 @@ fn file_row(path: &SharedString, hits: &[Range<usize>], cx: &App) -> Row {
|
||||
let folder = &path[..name_at.saturating_sub(1)];
|
||||
Row {
|
||||
value: path.clone(),
|
||||
name: path[name_at..].to_string().into(),
|
||||
icon: Some(IconName::File),
|
||||
label: marked(
|
||||
path[name_at..].to_string().into(),
|
||||
@@ -260,7 +264,8 @@ fn file_row(path: &SharedString, hits: &[Range<usize>], cx: &App) -> Row {
|
||||
|
||||
impl RenderOnce for QuickOpen {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = query_field(&self.id, "Go to file", window, cx);
|
||||
let placeholder = i18n::text(cx, "palette.open.placeholder", &[]);
|
||||
let input = query_field(&self.id, placeholder, window, cx);
|
||||
let query = input.read(cx).text().trim().to_string();
|
||||
let (group, empty) = if query.is_empty() {
|
||||
let rows = self
|
||||
@@ -270,10 +275,10 @@ impl RenderOnce for QuickOpen {
|
||||
.collect();
|
||||
(
|
||||
Group {
|
||||
title: Some("Recent".into()),
|
||||
title: Some(i18n::text(cx, "palette.recent", &[])),
|
||||
rows,
|
||||
},
|
||||
"Type part of a file name",
|
||||
i18n::text(cx, "palette.open.idle", &[]),
|
||||
)
|
||||
} else {
|
||||
let rows = ranked(self.paths.iter(), &query, |path| &path[..])
|
||||
@@ -281,14 +286,17 @@ impl RenderOnce for QuickOpen {
|
||||
.take(SHOWN)
|
||||
.map(|(path, fit)| file_row(path, &fit.hits, cx))
|
||||
.collect();
|
||||
(Group { title: None, rows }, "No matching files")
|
||||
(
|
||||
Group { title: None, rows },
|
||||
i18n::text(cx, "palette.open.empty", &[]),
|
||||
)
|
||||
};
|
||||
Palette {
|
||||
id: self.id,
|
||||
input,
|
||||
groups: vec![group],
|
||||
start: 0,
|
||||
empty: empty.into(),
|
||||
empty,
|
||||
on_pick: self.on_open,
|
||||
on_close: self.on_close,
|
||||
}
|
||||
@@ -336,10 +344,12 @@ impl QuickSwitcher {
|
||||
|
||||
impl RenderOnce for QuickSwitcher {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = query_field(&self.id, "Switch to", window, cx);
|
||||
let placeholder = i18n::text(cx, "palette.switch.placeholder", &[]);
|
||||
let input = query_field(&self.id, placeholder, window, cx);
|
||||
let query = input.read(cx).text().trim().to_string();
|
||||
let row = |item: &Choice, hits: Vec<Range<usize>>| Row {
|
||||
value: item.value.clone(),
|
||||
name: item.label.clone(),
|
||||
icon: item.icon,
|
||||
label: marked(item.label.clone(), hits, cx),
|
||||
detail: None,
|
||||
@@ -367,7 +377,7 @@ impl RenderOnce for QuickSwitcher {
|
||||
input,
|
||||
groups: vec![Group { title: None, rows }],
|
||||
start,
|
||||
empty: "Nothing open by that name".into(),
|
||||
empty: i18n::text(cx, "palette.switch.empty", &[]),
|
||||
on_pick: self.on_switch,
|
||||
on_close: self.on_close,
|
||||
}
|
||||
@@ -424,7 +434,8 @@ impl SearchPalette {
|
||||
}
|
||||
|
||||
pub(crate) fn palette(self, window: &mut Window, cx: &mut App) -> Palette {
|
||||
let input = query_field(&self.id, "Search everything", window, cx);
|
||||
let placeholder = i18n::text(cx, "palette.search.placeholder", &[]);
|
||||
let input = query_field(&self.id, placeholder, window, cx);
|
||||
let query = input.read(cx).text().trim().to_string();
|
||||
let groups = (self.search)(&query)
|
||||
.into_iter()
|
||||
@@ -441,7 +452,8 @@ impl SearchPalette {
|
||||
);
|
||||
})
|
||||
.map(|result| Row {
|
||||
value: result.value,
|
||||
value: result.value.clone(),
|
||||
name: result.label.clone(),
|
||||
icon: result.icon,
|
||||
label: Highlight::matching(result.label, &query).into_any_element(),
|
||||
detail: result
|
||||
@@ -458,9 +470,9 @@ impl SearchPalette {
|
||||
groups,
|
||||
start: 0,
|
||||
empty: if query.is_empty() {
|
||||
"Type to search".into()
|
||||
i18n::text(cx, "palette.search.idle", &[])
|
||||
} else {
|
||||
format!("Nothing found for “{query}”").into()
|
||||
i18n::text(cx, "palette.search.empty", &[("query", &query)])
|
||||
},
|
||||
on_pick: self.on_open,
|
||||
on_close: self.on_close,
|
||||
|
||||
@@ -7,8 +7,8 @@ use std::{ops::Range, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, ElementId, Entity, FontWeight, HighlightStyle,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Point, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, StyledText, Window, div, prelude::*,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Point, Role, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, StyledText, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -102,6 +102,8 @@ pub(crate) fn marked(text: SharedString, hits: Vec<Range<usize>>, cx: &App) -> A
|
||||
/// One result: the value a pick hands back, then what the row shows.
|
||||
pub(crate) struct Row {
|
||||
pub value: SharedString,
|
||||
/// The row's plain name, for assistive technology.
|
||||
pub name: SharedString,
|
||||
pub icon: Option<IconName>,
|
||||
pub label: AnyElement,
|
||||
pub detail: Option<AnyElement>,
|
||||
@@ -125,7 +127,7 @@ struct Cursor {
|
||||
/// The query field kept for palette `id`.
|
||||
pub(crate) fn query_field(
|
||||
id: &ElementId,
|
||||
placeholder: &'static str,
|
||||
placeholder: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<TextInput> {
|
||||
@@ -250,6 +252,9 @@ impl Palette {
|
||||
children.push(
|
||||
div()
|
||||
.id(("row", ix))
|
||||
.role(Role::ListBoxOption)
|
||||
.aria_selected(ix == at)
|
||||
.aria_label(row.name.clone())
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
@@ -328,11 +333,9 @@ impl Palette {
|
||||
cx.stop_propagation();
|
||||
down(1, cx);
|
||||
})
|
||||
.capture_action(|_: &Enter, _, cx| cx.stop_propagation())
|
||||
.on_key_up(move |event, window, cx| {
|
||||
let stroke = &event.keystroke;
|
||||
if stroke.key == "enter" && !stroke.modifiers.modified() && count > 0 {
|
||||
cx.stop_propagation();
|
||||
.capture_action(move |_: &Enter, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if count > 0 {
|
||||
enter(chosen.read(cx).at.min(count - 1), window, cx);
|
||||
}
|
||||
})
|
||||
@@ -363,6 +366,7 @@ impl Palette {
|
||||
.child(
|
||||
div()
|
||||
.id((self.id.clone(), "list"))
|
||||
.role(Role::ListBox)
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, ElementId, FontWeight, InteractiveElement,
|
||||
IntoElement, MouseButton, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement,
|
||||
Styled, Window, div, prelude::*,
|
||||
IntoElement, MouseButton, ParentElement, RenderOnce, Role, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -150,6 +150,9 @@ impl RenderOnce for Tabs {
|
||||
let pick = pick.clone();
|
||||
div()
|
||||
.id(("tab", ix))
|
||||
.role(Role::Tab)
|
||||
.aria_selected(on)
|
||||
.aria_label(tab.label.clone())
|
||||
.relative()
|
||||
.flex()
|
||||
.items_center()
|
||||
@@ -195,6 +198,7 @@ impl RenderOnce for Tabs {
|
||||
let (keys, key_pick) = (tabs.clone(), pick.clone());
|
||||
let strip = div()
|
||||
.id((self.id.clone(), "strip"))
|
||||
.role(Role::TabList)
|
||||
.track_focus(&focus)
|
||||
.relative()
|
||||
.flex()
|
||||
|
||||
+7
-13
@@ -69,7 +69,7 @@ impl Dialog {
|
||||
self
|
||||
}
|
||||
|
||||
/// Runs on an unmodified Enter release while the `focus_first` field holds focus; `build` gets the dialog's close.
|
||||
/// Runs on an unmodified Enter while the `focus_first` field holds focus; `build` gets the dialog's close. The field's key binding fires on key-down, so an Enter an IME takes never reaches it.
|
||||
pub(crate) fn on_enter(mut self, build: impl FnOnce(Close) -> Run + 'static) -> Self {
|
||||
self.enter = Some(Box::new(build));
|
||||
self
|
||||
@@ -254,18 +254,12 @@ impl RenderOnce for Dialog {
|
||||
}
|
||||
})
|
||||
.when_some(enter, |layer, (field, enter)| {
|
||||
layer
|
||||
.capture_action(|_: &Enter, _, cx| cx.stop_propagation())
|
||||
.on_key_up(move |event, window, cx| {
|
||||
let stroke = &event.keystroke;
|
||||
if stroke.key == "enter"
|
||||
&& !stroke.modifiers.modified()
|
||||
&& field.is_focused(window)
|
||||
{
|
||||
cx.stop_propagation();
|
||||
enter(window, cx);
|
||||
}
|
||||
})
|
||||
layer.capture_action(move |_: &Enter, window, cx| {
|
||||
cx.stop_propagation();
|
||||
if field.is_focused(window) {
|
||||
enter(window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.child(
|
||||
FocusScope::new(&focus)
|
||||
|
||||
+12
-2
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, ParentElement,
|
||||
RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, div,
|
||||
prelude::*,
|
||||
RenderOnce, Role, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
||||
div, prelude::*,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
@@ -220,6 +220,11 @@ impl RenderOnce for StatusBarItem {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let hover = theme.colors.hover;
|
||||
let name = self
|
||||
.label
|
||||
.clone()
|
||||
.or_else(|| self.tooltip.clone())
|
||||
.or_else(|| self.icon.map(|icon| icon.name().into()));
|
||||
div()
|
||||
.id(self.id)
|
||||
.flex()
|
||||
@@ -239,6 +244,11 @@ impl RenderOnce for StatusBarItem {
|
||||
.when_some(self.label, |item, label| item.child(label))
|
||||
.when_some(self.tooltip, |item, text| item.tooltip(Tooltip::text(text)))
|
||||
.when_some(self.on_click, |item, handler| {
|
||||
let item = item.role(Role::Button);
|
||||
let item = match name {
|
||||
Some(name) => item.aria_label(name),
|
||||
None => item,
|
||||
};
|
||||
item.cursor_pointer()
|
||||
.hover(|style| style.bg(hover))
|
||||
.on_click(handler)
|
||||
|
||||
@@ -5,6 +5,7 @@ use gpui::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
i18n,
|
||||
navigation::{Group, Palette, Row, fuzzy, marked, query_field},
|
||||
primitives::IconName,
|
||||
theme::ActiveTheme,
|
||||
@@ -57,7 +58,12 @@ impl CommandHistory {
|
||||
|
||||
impl RenderOnce for CommandHistory {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let input = query_field(&self.id, "Search past commands", window, cx);
|
||||
let input = query_field(
|
||||
&self.id,
|
||||
i18n::text(cx, "palette.history.placeholder", &[]),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let query = input.read(cx).text().trim().to_string();
|
||||
let colors = cx.theme().colors.clone();
|
||||
let mut found: Vec<(i32, Row)> = self
|
||||
@@ -79,6 +85,7 @@ impl RenderOnce for CommandHistory {
|
||||
score,
|
||||
Row {
|
||||
value: past.command.clone(),
|
||||
name: past.command.clone(),
|
||||
icon: Some(icon),
|
||||
label: marked(past.command.clone(), hits, cx),
|
||||
detail: Some(
|
||||
@@ -108,7 +115,7 @@ impl RenderOnce for CommandHistory {
|
||||
rows: found.into_iter().map(|(_, row)| row).collect(),
|
||||
}],
|
||||
start: 0,
|
||||
empty: "No past command fits".into(),
|
||||
empty: i18n::text(cx, "palette.history.empty", &[]),
|
||||
on_pick: self.on_pick,
|
||||
on_close: self.on_close,
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user