chore: audit components

This commit is contained in:
2026-11-10 15:06:16 +07:00
parent af2177c6c8
commit 03369f501e
92 changed files with 2744 additions and 1947 deletions
+105 -41
View File
@@ -2,12 +2,13 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role,
SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _, Window,
App, ElementId, InteractiveElement as _, IntoElement, KeyDownEvent, ParentElement as _,
RenderOnce, Role, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled as _,
Window, canvas,
};
use gpui_native_icons::{Icon, IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use gpui_native_utils::focus_ring;
use gpui_native_utils::{focus_handle, focus_ring};
use crate::common::{Sizable, StyledExt, h_flex};
use crate::menu::{Menu, MenuItem};
@@ -17,6 +18,7 @@ pub type SelectHandler<T> = Rc<dyn Fn(Option<T>, &mut Window, &mut App)>;
/// Select option for a [`Select`] control.
#[derive(Clone)]
#[must_use]
pub struct SelectOption<T> {
value: T,
label: SharedString,
@@ -61,6 +63,7 @@ where
/// A control that shows the chosen value and opens a menu of the choices.
#[derive(IntoElement)]
#[must_use]
pub struct Select<T>
where
T: Clone + PartialEq + 'static,
@@ -107,7 +110,10 @@ where
}
/// Sets the current value. `None` shows the placeholder.
pub fn selected(mut self, selected: impl Into<Option<T>>) -> Self {
pub fn selected<S>(mut self, selected: S) -> Self
where
S: Into<Option<T>>,
{
self.selected = selected.into();
self
}
@@ -187,6 +193,16 @@ where
.clone()
});
let focus = focus_handle(&self.id, window, cx);
// Set when the popup closes without an activation; the trigger claims
// focus at paint, leaving keyboard activations to drop focus in place.
let refocus_pending = window.with_element_namespace(self.id.clone(), |window| {
window
.use_keyed_state("select-refocus", cx, |_, _| false)
.clone()
});
let is_open = *open.read(cx);
let open_for_items = open.clone();
@@ -220,54 +236,94 @@ where
let trigger_label = label.clone();
let trigger_style = self.style;
let trigger = move |_window: &mut Window, _cx: &mut App| {
h_flex()
.id((trigger_id.clone(), "select-trigger"))
.debug_selector(|| "select-trigger".to_string())
.role(Role::ComboBox)
.aria_label(trigger_label.clone())
.cursor_pointer()
.justify_between()
.gap_2()
.w_full()
.h(metrics.input_height + BORDER_WIDTH * 2.0)
.px(metrics.dense_padding_x)
.rounded(radius)
.border_1()
.border_color(rest_border)
.bg(input)
.text_size(metrics.text_size)
.text_color(if trigger_has_value {
foreground
} else {
muted_foreground
})
.when(is_open && focus_ring_on, |trigger| {
trigger.shadow(focus_ring(ring))
})
.hover(|style| style.bg(muted).border_color(hover_border))
.child(trigger_text.clone())
.child(
icon(IconRole::ChevronDown)
.size(icon_size)
.color(muted_foreground),
)
.refine_style(&trigger_style)
.into_any_element()
let trigger = {
let open = open.clone();
let focus = focus.clone();
let refocus_for_trigger = refocus_pending.clone();
move |_window: &mut Window, _cx: &mut App| {
h_flex()
.id((trigger_id.clone(), "select-trigger"))
.debug_selector(|| "select-trigger".to_string())
.role(Role::ComboBox)
.aria_label(trigger_label.clone())
.aria_expanded(is_open)
.track_focus(&focus)
.tab_stop(true)
.focus_visible(move |style| style.shadow(focus_ring(ring)))
.on_key_down({
let open = open.clone();
move |event: &KeyDownEvent, _window, cx| {
if !matches!(event.keystroke.key.as_str(), "enter" | "space") {
return;
}
open.update(cx, |open, _| *open = !*open);
cx.stop_propagation();
}
})
.cursor_pointer()
.justify_between()
.gap_2()
.w_full()
.h(metrics.input_height + BORDER_WIDTH * 2.0)
.px(metrics.dense_padding_x)
.rounded(radius)
.border_1()
.border_color(rest_border)
.bg(input)
.text_size(metrics.text_size)
.text_color(if trigger_has_value {
foreground
} else {
muted_foreground
})
.when(is_open && focus_ring_on, |trigger| {
trigger.shadow(focus_ring(ring))
})
.hover(|style| style.bg(muted).border_color(hover_border))
.child(trigger_text.clone())
.child(
icon(IconRole::ChevronDown)
.size(icon_size)
.color(muted_foreground),
)
.child(
canvas(|_, _, _| {}, {
let focus = focus.clone();
let refocus_pending = refocus_for_trigger.clone();
move |_, _, window, cx| {
if *refocus_pending.read(cx) {
refocus_pending.update(cx, |pending, _| *pending = false);
window.focus(&focus, cx);
}
}
})
.absolute()
.inset_0(),
)
.refine_style(&trigger_style)
.into_any_element()
}
};
let select_id = self.id.clone();
let refocus_for_items = refocus_pending.clone();
let items = move |_window: &mut Window, _cx: &mut App| {
self.options
.iter()
.map(|option| {
.enumerate()
.map(|(index, option)| {
let is_selected = self.selected.as_ref() == Some(&option.value);
let handler = {
let open = open_for_items.clone();
let refocus_pending = refocus_for_items.clone();
let value = option.value.clone();
let on_change = self.on_change.clone();
move |window: &mut Window, cx: &mut App| {
open.update(cx, |open, _| *open = false);
// Activations own the close, so no focus returns to the trigger.
refocus_pending.update(cx, |pending, _| *pending = false);
if let Some(on_change) = &on_change {
on_change(Some(value.clone()), window, cx);
}
@@ -275,6 +331,7 @@ where
};
MenuItem::new(option.label.clone())
.id((select_id.clone(), format!("option-{index}")))
.when_some(option.icon.clone(), |row, icon| row.icon(icon))
.checked(is_selected)
.handler(handler)
@@ -284,8 +341,15 @@ where
let menu = Menu::new(self.id)
.open(is_open)
.on_change(move |next, _window, cx| {
open.update(cx, |open, _| *open = next);
.on_change({
let open = open.clone();
let refocus_pending = refocus_pending.clone();
move |next, _window, cx| {
if !next {
refocus_pending.update(cx, |pending, _| *pending = true);
}
open.update(cx, |open, _| *open = next);
}
})
.trigger(trigger)
.items(items);