Make search field in panels be at the top (#43200)

This mostly affects the collab and outline panels for now. It has always
been a bit weird that the search field was at the bottom of the panel,
even more so because in both cases, you can _arrow down_ to start
navigating the list with your keyboard. So, with the search at the
bottom, you'd arrow down and get to the top of the list, which was very
strange. Now, with it at the top, it not only looks better but it is
also more generally consistent with other surfaces in the app, like
pickers, the settings UI, rules library, etc. Most search fields are
always at the top.

<img width="800" height="1830" alt="image"
src="https://github.com/user-attachments/assets/3e2c3b8f-5907-4d83-8804-b3fc77342103"
/>

Release Notes:

- N/A
This commit is contained in:
Danilo Leal
2025-11-20 18:57:22 -03:00
committed by GitHub
parent 9667d7882a
commit 361fcc5c90
2 changed files with 81 additions and 78 deletions
+17 -11
View File
@@ -32,7 +32,7 @@ use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings};
use ui::{
Avatar, AvatarAvailabilityIndicator, Button, Color, ContextMenu, Facepile, HighlightedLabel,
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tooltip,
Icon, IconButton, IconName, IconSize, Indicator, Label, ListHeader, ListItem, Tab, Tooltip,
prelude::*, tooltip_container,
};
use util::{ResultExt, TryFutureExt, maybe};
@@ -287,7 +287,7 @@ impl CollabPanel {
cx.new(|cx| {
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Filter...", window, cx);
editor.set_placeholder_text("Search channels…", window, cx);
editor
});
@@ -2412,6 +2412,21 @@ impl CollabPanel {
});
v_flex()
.size_full()
.gap_1()
.child(
h_flex()
.p_2()
.h(Tab::container_height(cx))
.gap_1p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::MagnifyingGlass)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(self.render_filter_input(&self.filter_editor, cx)),
)
.child(
list(
self.list_state.clone(),
@@ -2419,15 +2434,6 @@ impl CollabPanel {
)
.size_full(),
)
.child(
v_flex()
.child(div().mx_2().border_primary(cx).border_t_1())
.child(
v_flex()
.p_2()
.child(self.render_filter_input(&self.filter_editor, cx)),
),
)
}
fn render_filter_input(
+64 -67
View File
@@ -46,10 +46,8 @@ use settings::{Settings, SettingsStore};
use smol::channel;
use theme::{SyntaxTheme, ThemeSettings};
use ui::{
ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, DynamicSpacing, FluentBuilder,
HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, IndentGuideColors,
IndentGuideLayout, Label, LabelCommon, ListItem, ScrollAxes, Scrollbars, StyledExt,
StyledTypography, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex,
ContextMenu, FluentBuilder, HighlightedLabel, IconButton, IconButtonShape, IndentGuideColors,
IndentGuideLayout, ListItem, ScrollAxes, Scrollbars, Tab, Tooltip, WithScrollbar, prelude::*,
};
use util::{RangeExt, ResultExt, TryFutureExt, debug_panic, rel_path::RelPath};
use workspace::{
@@ -714,7 +712,7 @@ impl OutlinePanel {
cx.new(|cx| {
let filter_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Filter...", window, cx);
editor.set_placeholder_text("Search buffer symbols…", window, cx);
editor
});
let filter_update_subscription = cx.subscribe_in(
@@ -4553,6 +4551,7 @@ impl OutlinePanel {
v_flex()
.id("empty-outline-state")
.gap_0p5()
.flex_1()
.justify_center()
.size_full()
@@ -4560,25 +4559,28 @@ impl OutlinePanel {
panel
.child(h_flex().justify_center().child(Label::new(header)))
.when_some(query.clone(), |panel, query| {
panel.child(h_flex().justify_center().child(Label::new(query)))
panel.child(
h_flex()
.px_0p5()
.justify_center()
.bg(cx.theme().colors().element_selected.opacity(0.2))
.child(Label::new(query)),
)
})
.child(
h_flex()
.pt(DynamicSpacing::Base04.rems(cx))
.justify_center()
.child({
let keystroke =
match self.position(window, cx) {
DockPosition::Left => window
.keystroke_text_for(&workspace::ToggleLeftDock),
DockPosition::Bottom => window
.keystroke_text_for(&workspace::ToggleBottomDock),
DockPosition::Right => window
.keystroke_text_for(&workspace::ToggleRightDock),
};
Label::new(format!("Toggle this panel with {keystroke}"))
}),
)
.child(h_flex().justify_center().child({
let keystroke = match self.position(window, cx) {
DockPosition::Left => {
window.keystroke_text_for(&workspace::ToggleLeftDock)
}
DockPosition::Bottom => {
window.keystroke_text_for(&workspace::ToggleBottomDock)
}
DockPosition::Right => {
window.keystroke_text_for(&workspace::ToggleRightDock)
}
};
Label::new(format!("Toggle Panel With {keystroke}")).color(Color::Muted)
}))
})
} else {
let list_contents = {
@@ -4728,39 +4730,37 @@ impl OutlinePanel {
}
fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
v_flex().flex_none().child(horizontal_separator(cx)).child(
h_flex()
.p_2()
.w_full()
.child(self.filter_editor.clone())
.child(
div().child(
IconButton::new(
"outline-panel-menu",
if pinned {
IconName::Unpin
} else {
IconName::Pin
},
)
.tooltip(Tooltip::text(if pinned {
"Unpin Outline"
} else {
"Pin Active Outline"
}))
.shape(IconButtonShape::Square)
.on_click(cx.listener(
|outline_panel, _, window, cx| {
outline_panel.toggle_active_editor_pin(
&ToggleActiveEditorPin,
window,
cx,
);
},
)),
),
),
)
let (icon, icon_tooltip) = if pinned {
(IconName::Unpin, "Unpin Outline")
} else {
(IconName::Pin, "Pin Active Outline")
};
h_flex()
.p_2()
.h(Tab::container_height(cx))
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.w_full()
.gap_1p5()
.child(
Icon::new(IconName::MagnifyingGlass)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(self.filter_editor.clone()),
)
.child(
IconButton::new("pin_button", icon)
.tooltip(Tooltip::text(icon_tooltip))
.shape(IconButtonShape::Square)
.on_click(cx.listener(|outline_panel, _, window, cx| {
outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, window, cx);
})),
)
}
fn buffers_inside_directory(
@@ -4974,6 +4974,8 @@ impl Render for OutlinePanel {
_ => None,
};
let search_query_text = search_query.map(|sq| sq.query.to_string());
v_flex()
.id("outline-panel")
.size_full()
@@ -5020,22 +5022,21 @@ impl Render for OutlinePanel {
}),
)
.track_focus(&self.focus_handle)
.when_some(search_query, |outline_panel, search_state| {
.child(self.render_filter_footer(pinned, cx))
.when_some(search_query_text, |outline_panel, query_text| {
outline_panel.child(
h_flex()
.py_1p5()
.px_2()
.h(DynamicSpacing::Base32.px(cx))
.flex_shrink_0()
.border_b_1()
.border_color(cx.theme().colors().border)
.h(Tab::container_height(cx))
.gap_0p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(Label::new("Searching:").color(Color::Muted))
.child(Label::new(search_state.query.to_string())),
.child(Label::new(query_text)),
)
})
.child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
.child(self.render_filter_footer(pinned, cx))
}
}
@@ -5214,10 +5215,6 @@ fn empty_icon() -> AnyElement {
.into_any_element()
}
fn horizontal_separator(cx: &mut App) -> Div {
div().mx_2().border_primary(cx).border_t_1()
}
#[derive(Debug, Default)]
struct GenerationState {
entries: Vec<CachedEntry>,