onboarding: Use a picker for the font dropdowns (#35638)

Release Notes:

- N/A
This commit is contained in:
Danilo Leal 2025-08-05 11:38:08 -03:00 committed by GitHub
parent 351e8c4cd9
commit 5940ed979f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 272 additions and 76 deletions

View file

@ -25,12 +25,14 @@ db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
fuzzy.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
menu.workspace = true
notifications.workspace = true
picker.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -2,14 +2,19 @@ use std::sync::Arc;
use editor::{EditorSettings, ShowMinimap};
use fs::Fs;
use gpui::{Action, App, FontFeatures, IntoElement, Pixels, Window};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Action, AnyElement, App, Context, FontFeatures, IntoElement, Pixels, SharedString, Task, Window,
};
use language::language_settings::{AllLanguageSettings, FormatOnSave};
use picker::{Picker, PickerDelegate};
use project::project_settings::ProjectSettings;
use settings::{Settings as _, update_settings_file};
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
use ui::{
ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup,
ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
ButtonLike, ListItem, ListItemSpacing, NumericStepper, PopoverMenu, SwitchField,
ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip,
prelude::*,
};
use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
@ -246,9 +251,25 @@ fn render_import_settings_section(cx: &App) -> impl IntoElement {
fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = theme_settings.ui_font_size(cx);
let font_family = theme_settings.buffer_font.family.clone();
let ui_font_family = theme_settings.ui_font.family.clone();
let buffer_font_family = theme_settings.buffer_font.family.clone();
let buffer_font_size = theme_settings.buffer_font_size(cx);
let ui_font_picker =
cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
let buffer_font_picker = cx.new(|cx| {
font_picker(
buffer_font_family.clone(),
write_buffer_font_family,
window,
cx,
)
});
let ui_font_handle = ui::PopoverMenuHandle::default();
let buffer_font_handle = ui::PopoverMenuHandle::default();
h_flex()
.w_full()
.gap_4()
@ -263,34 +284,35 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl
.justify_between()
.gap_2()
.child(
DropdownMenu::new(
"ui-font-family",
theme_settings.ui_font.family.clone(),
ContextMenu::build(window, cx, |mut menu, _, cx| {
let font_family_cache = FontFamilyCache::global(cx);
for font_name in font_family_cache.list_font_families(cx) {
menu = menu.custom_entry(
{
let font_name = font_name.clone();
move |_window, _cx| {
Label::new(font_name.clone()).into_any_element()
}
},
{
let font_name = font_name.clone();
move |_window, cx| {
write_ui_font_family(font_name.clone(), cx);
}
},
)
}
menu
}),
)
.style(ui::DropdownStyle::Outlined)
.full_width(true),
PopoverMenu::new("ui-font-picker")
.menu({
let ui_font_picker = ui_font_picker.clone();
move |_window, _cx| Some(ui_font_picker.clone())
})
.trigger(
ButtonLike::new("ui-font-family-button")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.full_width()
.child(
h_flex()
.w_full()
.justify_between()
.child(Label::new(ui_font_family))
.child(
Icon::new(IconName::ChevronUpDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.full_width(true)
.anchor(gpui::Corner::TopLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(4.0),
})
.with_handle(ui_font_handle),
)
.child(
NumericStepper::new(
@ -318,34 +340,35 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl
.justify_between()
.gap_2()
.child(
DropdownMenu::new(
"buffer-font-family",
font_family,
ContextMenu::build(window, cx, |mut menu, _, cx| {
let font_family_cache = FontFamilyCache::global(cx);
for font_name in font_family_cache.list_font_families(cx) {
menu = menu.custom_entry(
{
let font_name = font_name.clone();
move |_window, _cx| {
Label::new(font_name.clone()).into_any_element()
}
},
{
let font_name = font_name.clone();
move |_window, cx| {
write_buffer_font_family(font_name.clone(), cx);
}
},
)
}
menu
}),
)
.style(ui::DropdownStyle::Outlined)
.full_width(true),
PopoverMenu::new("buffer-font-picker")
.menu({
let buffer_font_picker = buffer_font_picker.clone();
move |_window, _cx| Some(buffer_font_picker.clone())
})
.trigger(
ButtonLike::new("buffer-font-family-button")
.style(ButtonStyle::Outlined)
.size(ButtonSize::Medium)
.full_width()
.child(
h_flex()
.w_full()
.justify_between()
.child(Label::new(buffer_font_family))
.child(
Icon::new(IconName::ChevronUpDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
),
)
.full_width(true)
.anchor(gpui::Corner::TopLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(4.0),
})
.with_handle(buffer_font_handle),
)
.child(
NumericStepper::new(
@ -364,6 +387,175 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl
)
}
type FontPicker = Picker<FontPickerDelegate>;
pub struct FontPickerDelegate {
fonts: Vec<SharedString>,
filtered_fonts: Vec<StringMatch>,
selected_index: usize,
current_font: SharedString,
on_font_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
}
impl FontPickerDelegate {
fn new(
current_font: SharedString,
on_font_changed: impl Fn(SharedString, &mut App) + 'static,
cx: &mut Context<FontPicker>,
) -> Self {
let font_family_cache = FontFamilyCache::global(cx);
let fonts: Vec<SharedString> = font_family_cache
.list_font_families(cx)
.into_iter()
.collect();
let selected_index = fonts
.iter()
.position(|font| *font == current_font)
.unwrap_or(0);
Self {
fonts: fonts.clone(),
filtered_fonts: fonts
.iter()
.enumerate()
.map(|(index, font)| StringMatch {
candidate_id: index,
string: font.to_string(),
positions: Vec::new(),
score: 0.0,
})
.collect(),
selected_index,
current_font,
on_font_changed: Arc::new(on_font_changed),
}
}
}
impl PickerDelegate for FontPickerDelegate {
type ListItem = AnyElement;
fn match_count(&self) -> usize {
self.filtered_fonts.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<FontPicker>) {
self.selected_index = ix.min(self.filtered_fonts.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search fonts…".into()
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
cx: &mut Context<FontPicker>,
) -> Task<()> {
let fonts = self.fonts.clone();
let current_font = self.current_font.clone();
let matches: Vec<StringMatch> = if query.is_empty() {
fonts
.iter()
.enumerate()
.map(|(index, font)| StringMatch {
candidate_id: index,
string: font.to_string(),
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
let _candidates: Vec<StringMatchCandidate> = fonts
.iter()
.enumerate()
.map(|(id, font)| StringMatchCandidate::new(id, font.as_ref()))
.collect();
fonts
.iter()
.enumerate()
.filter(|(_, font)| font.to_lowercase().contains(&query.to_lowercase()))
.map(|(index, font)| StringMatch {
candidate_id: index,
string: font.to_string(),
positions: Vec::new(),
score: 0.0,
})
.collect()
};
let selected_index = if query.is_empty() {
fonts
.iter()
.position(|font| *font == current_font)
.unwrap_or(0)
} else {
matches
.iter()
.position(|m| fonts[m.candidate_id] == current_font)
.unwrap_or(0)
};
self.filtered_fonts = matches;
self.selected_index = selected_index;
cx.notify();
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<FontPicker>) {
if let Some(font_match) = self.filtered_fonts.get(self.selected_index) {
let font = font_match.string.clone();
(self.on_font_changed)(font.into(), cx);
}
}
fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<FontPicker>) {}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<FontPicker>,
) -> Option<Self::ListItem> {
let font_match = self.filtered_fonts.get(ix)?;
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(Label::new(font_match.string.clone()))
.into_any_element(),
)
}
}
fn font_picker(
current_font: SharedString,
on_font_changed: impl Fn(SharedString, &mut App) + 'static,
window: &mut Window,
cx: &mut Context<FontPicker>,
) -> FontPicker {
let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx);
Picker::list(delegate, window, cx)
.show_scrollbar(true)
.width(rems_from_px(210.))
.max_height(Some(rems(20.).into()))
}
fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠.";