From 361fcc5c90a4d7d7cf9e7475e1cf7e0f1369f12a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:57:22 -0300 Subject: [PATCH] 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. image Release Notes: - N/A --- crates/collab_ui/src/collab_panel.rs | 28 +++-- crates/outline_panel/src/outline_panel.rs | 131 +++++++++++----------- 2 files changed, 81 insertions(+), 78 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 8f2e959b17293af66279793e73fefceea413ca49..618348307f1270e180faf4b1d061b9a942e39fa5 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -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( diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index f44c6438ebd454d343a8ac49b0f6db11c11b469d..36cd9d076bb428f37c898a142fa7f3d1da887918 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -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) -> 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,