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:
ZacharyZhang-NY committed 2026-10-02 12:58:43 -04:00
1 parent b54500b0e4
commit 290939f82f
15 files changed
+257 -75

No files matched your search

+5 -1
View File
@@ -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()
+8 -2
View File
@@ -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()
+9 -2
View File
@@ -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
View File
@@ -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,
}
+7 -2
View File
@@ -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
View File
@@ -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),
),
]
+87
View File
@@ -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)));
}
}
+27
View File
@@ -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());
+5 -2
View File
@@ -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()
+26 -14
View File
@@ -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,
+12 -8
View File
@@ -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()
+6 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+9 -2
View File
@@ -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,
}