audit combobox

This commit is contained in:
2026-11-08 11:00:39 +07:00
parent 21d32736a8
commit abb56567e4
7 changed files with 315 additions and 257 deletions
+61 -12
View File
@@ -12,13 +12,13 @@ use gpui::prelude::FluentBuilder as _;
use gpui::{
App, Bounds, Context, ElementId, Entity, EventEmitter, InteractiveElement as _, IntoElement,
KeyBinding, MouseButton, MouseDownEvent, ParentElement as _, Pixels, Render, Role,
ScrollHandle, SharedString, StatefulInteractiveElement as _, Styled as _, Subscription, Task,
Window, actions, canvas, deferred, div, px,
ScrollHandle, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled,
Subscription, Task, Window, actions, anchored, canvas, deferred, div, point, px,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, Size};
use crate::common::{Sizable, h_flex, v_flex};
use crate::common::{Sizable, StyledExt as _, h_flex, v_flex};
use crate::input::{Input, InputState, InputStateEvent};
use crate::scroll_area::ScrollArea;
@@ -81,6 +81,7 @@ where
filter: Option<ComboboxFilter<T>>,
unmatched: UnmatchedText,
size: Size,
style: StyleRefinement,
}
impl<T> Combobox<T>
@@ -103,6 +104,7 @@ where
filter: None,
unmatched: UnmatchedText::default(),
size: Size::default(),
style: StyleRefinement::default(),
}
}
@@ -165,6 +167,16 @@ where
}
}
/// Style refinements set the combobox's width.
impl<T> Styled for Combobox<T>
where
T: Clone + PartialEq + 'static,
{
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
/// Emitted when the combobox's value changes.
pub struct ComboboxChanged;
@@ -189,6 +201,8 @@ where
filter: ComboboxFilter<T>,
unmatched: UnmatchedText,
size: Size,
/// Width refinements from the builder, applied to the wrapper each render.
style: StyleRefinement,
/// Handler reports in flight; held so they die with the state.
tasks: Vec<Task<()>>,
/// Keeps the query-event subscription tied to the state's lifetime.
@@ -225,6 +239,7 @@ where
let subscription =
cx.subscribe_in(&query, window, |state, _, event, window, cx| match event {
InputStateEvent::Focused => state.query_focused(window, cx),
InputStateEvent::TextChanged => state.text_changed(window, cx),
InputStateEvent::Blurred => state.blurred(window, cx),
_ => {}
@@ -248,6 +263,7 @@ where
}),
unmatched: combobox.unmatched,
size: combobox.size,
style: combobox.style,
tasks: Vec::new(),
_subscription: subscription,
}
@@ -364,6 +380,16 @@ where
cx.notify();
}
/// Gaining focus.
fn query_focused(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
if self.selected.is_some() {
return;
}
self.refilter("");
self.highlight = (!self.visible.is_empty()).then_some(0usize);
cx.notify();
}
/// Losing focus. [`UnmatchedText`] decides what the text becomes.
fn blurred(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.highlight = None;
@@ -447,11 +473,25 @@ where
T: Clone + PartialEq + 'static,
{
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (metrics, colors, radius) = {
let (metrics, colors, radius, panel_border, spacing_unit, shadows) = {
let theme = cx.theme();
let metrics = theme.sizes.resolve(self.size);
(metrics, theme.colors, theme.radius)
let panel_border = if theme.borders.overlays {
theme.colors.border
} else {
theme.colors.surface
};
(
metrics,
theme.colors,
theme.radius,
panel_border,
theme.spacing.unit,
theme.elevations.popover.shadows(),
)
};
let icon_size = metrics.dense_icon_size;
let id = self.id.clone();
let state = cx.entity();
@@ -490,7 +530,9 @@ where
.id(id.clone())
.key_context(COMBOBOX_CONTEXT)
.relative()
.w_full()
.w(window.rem_size() * 12.0)
.min_w(window.rem_size() * 6.0)
.refine_style(&self.style)
.role(Role::ComboBox)
.aria_expanded(open)
.aria_label(self.name.clone())
@@ -532,13 +574,14 @@ where
.child(
Input::new((id.clone(), "query"), self.query.clone())
.placeholder(self.placeholder.clone())
.placeholder_when_focused(true)
.with_size(self.size),
)
.child(
h_flex()
.absolute()
.right(metrics.dense_padding_x)
.top((metrics.input_height - icon_size) / 2.)
.top((metrics.input_height + BORDER_WIDTH * 2.0 - icon_size) / 2.)
.child(chevron),
);
@@ -626,6 +669,10 @@ where
};
let panel_bounds: Rc<RefCell<Option<Bounds<Pixels>>>> = Rc::new(RefCell::new(None));
let popup_width = wrapper_bounds
.borrow()
.map(|bounds| bounds.size.width)
.unwrap_or_else(|| window.rem_size() * 12.0);
let panel = v_flex()
.id((id.clone(), "panel"))
@@ -633,14 +680,14 @@ where
let name = format!("{id}-panel");
move || name
})
.mt_1()
.w_full()
.w(popup_width)
.p_1()
.bg(colors.surface)
.text_color(colors.foreground)
.rounded(radius.overlay)
.border_1()
.border_color(colors.border)
.border_color(panel_border)
.shadow(shadows)
.overflow_hidden()
.child(rows)
.child(
@@ -684,7 +731,9 @@ where
.inset_0(),
);
wrapper = wrapper.child(deferred(panel));
wrapper = wrapper.child(deferred(
anchored().offset(point(px(0.), spacing_unit)).child(panel),
));
}
wrapper