onboarding: Use a picker for the font dropdowns (#35638)
Release Notes: - N/A
This commit is contained in:
parent
351e8c4cd9
commit
5940ed979f
6 changed files with 272 additions and 76 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -11039,12 +11039,14 @@ dependencies = [
|
||||||
"editor",
|
"editor",
|
||||||
"feature_flags",
|
"feature_flags",
|
||||||
"fs",
|
"fs",
|
||||||
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"language",
|
"language",
|
||||||
"language_model",
|
"language_model",
|
||||||
"menu",
|
"menu",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -25,12 +25,14 @@ db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
notifications.workspace = true
|
notifications.workspace = true
|
||||||
|
picker.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
|
|
@ -2,14 +2,19 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use editor::{EditorSettings, ShowMinimap};
|
use editor::{EditorSettings, ShowMinimap};
|
||||||
use fs::Fs;
|
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 language::language_settings::{AllLanguageSettings, FormatOnSave};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::project_settings::ProjectSettings;
|
use project::project_settings::ProjectSettings;
|
||||||
use settings::{Settings as _, update_settings_file};
|
use settings::{Settings as _, update_settings_file};
|
||||||
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
|
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
|
||||||
use ui::{
|
use ui::{
|
||||||
ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup,
|
ButtonLike, ListItem, ListItemSpacing, NumericStepper, PopoverMenu, SwitchField,
|
||||||
ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
|
ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip,
|
||||||
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
|
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 {
|
fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let theme_settings = ThemeSettings::get_global(cx);
|
let theme_settings = ThemeSettings::get_global(cx);
|
||||||
let ui_font_size = theme_settings.ui_font_size(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 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()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_4()
|
.gap_4()
|
||||||
|
@ -263,34 +284,35 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
DropdownMenu::new(
|
PopoverMenu::new("ui-font-picker")
|
||||||
"ui-font-family",
|
.menu({
|
||||||
theme_settings.ui_font.family.clone(),
|
let ui_font_picker = ui_font_picker.clone();
|
||||||
ContextMenu::build(window, cx, |mut menu, _, cx| {
|
move |_window, _cx| Some(ui_font_picker.clone())
|
||||||
let font_family_cache = FontFamilyCache::global(cx);
|
})
|
||||||
|
.trigger(
|
||||||
for font_name in font_family_cache.list_font_families(cx) {
|
ButtonLike::new("ui-font-family-button")
|
||||||
menu = menu.custom_entry(
|
.style(ButtonStyle::Outlined)
|
||||||
{
|
.size(ButtonSize::Medium)
|
||||||
let font_name = font_name.clone();
|
.full_width()
|
||||||
move |_window, _cx| {
|
.child(
|
||||||
Label::new(font_name.clone()).into_any_element()
|
h_flex()
|
||||||
}
|
.w_full()
|
||||||
},
|
.justify_between()
|
||||||
{
|
.child(Label::new(ui_font_family))
|
||||||
let font_name = font_name.clone();
|
.child(
|
||||||
move |_window, cx| {
|
Icon::new(IconName::ChevronUpDown)
|
||||||
write_ui_font_family(font_name.clone(), cx);
|
.color(Color::Muted)
|
||||||
}
|
.size(IconSize::XSmall),
|
||||||
},
|
),
|
||||||
)
|
),
|
||||||
}
|
)
|
||||||
|
.full_width(true)
|
||||||
menu
|
.anchor(gpui::Corner::TopLeft)
|
||||||
}),
|
.offset(gpui::Point {
|
||||||
)
|
x: px(0.0),
|
||||||
.style(ui::DropdownStyle::Outlined)
|
y: px(4.0),
|
||||||
.full_width(true),
|
})
|
||||||
|
.with_handle(ui_font_handle),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
NumericStepper::new(
|
NumericStepper::new(
|
||||||
|
@ -318,34 +340,35 @@ fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
DropdownMenu::new(
|
PopoverMenu::new("buffer-font-picker")
|
||||||
"buffer-font-family",
|
.menu({
|
||||||
font_family,
|
let buffer_font_picker = buffer_font_picker.clone();
|
||||||
ContextMenu::build(window, cx, |mut menu, _, cx| {
|
move |_window, _cx| Some(buffer_font_picker.clone())
|
||||||
let font_family_cache = FontFamilyCache::global(cx);
|
})
|
||||||
|
.trigger(
|
||||||
for font_name in font_family_cache.list_font_families(cx) {
|
ButtonLike::new("buffer-font-family-button")
|
||||||
menu = menu.custom_entry(
|
.style(ButtonStyle::Outlined)
|
||||||
{
|
.size(ButtonSize::Medium)
|
||||||
let font_name = font_name.clone();
|
.full_width()
|
||||||
move |_window, _cx| {
|
.child(
|
||||||
Label::new(font_name.clone()).into_any_element()
|
h_flex()
|
||||||
}
|
.w_full()
|
||||||
},
|
.justify_between()
|
||||||
{
|
.child(Label::new(buffer_font_family))
|
||||||
let font_name = font_name.clone();
|
.child(
|
||||||
move |_window, cx| {
|
Icon::new(IconName::ChevronUpDown)
|
||||||
write_buffer_font_family(font_name.clone(), cx);
|
.color(Color::Muted)
|
||||||
}
|
.size(IconSize::XSmall),
|
||||||
},
|
),
|
||||||
)
|
),
|
||||||
}
|
)
|
||||||
|
.full_width(true)
|
||||||
menu
|
.anchor(gpui::Corner::TopLeft)
|
||||||
}),
|
.offset(gpui::Point {
|
||||||
)
|
x: px(0.0),
|
||||||
.style(ui::DropdownStyle::Outlined)
|
y: px(4.0),
|
||||||
.full_width(true),
|
})
|
||||||
|
.with_handle(buffer_font_handle),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
NumericStepper::new(
|
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 {
|
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 ≠.";
|
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 ≠.";
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,7 @@ where
|
||||||
{
|
{
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
let picker = self.picker.clone();
|
let picker = self.picker.clone();
|
||||||
|
|
||||||
PopoverMenu::new("popover-menu")
|
PopoverMenu::new("popover-menu")
|
||||||
.menu(move |_window, _cx| Some(picker.clone()))
|
.menu(move |_window, _cx| Some(picker.clone()))
|
||||||
.trigger_with_tooltip(self.trigger, self.tooltip)
|
.trigger_with_tooltip(self.trigger, self.tooltip)
|
||||||
|
|
|
@ -276,25 +276,25 @@ impl RenderOnce for DropdownMenuTrigger {
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.rounded_sm()
|
.rounded_sm()
|
||||||
.bg(style.bg)
|
.map(|this| {
|
||||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
if self.full_width {
|
||||||
|
this.w_full()
|
||||||
|
} else {
|
||||||
|
this.flex_none().w_auto()
|
||||||
|
}
|
||||||
|
})
|
||||||
.when(is_outlined, |this| {
|
.when(is_outlined, |this| {
|
||||||
this.border_1()
|
this.border_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
})
|
})
|
||||||
.map(|el| {
|
.map(|this| {
|
||||||
if self.full_width {
|
|
||||||
el.w_full()
|
|
||||||
} else {
|
|
||||||
el.flex_none().w_auto()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(|el| {
|
|
||||||
if disabled {
|
if disabled {
|
||||||
el.cursor_not_allowed()
|
this.cursor_not_allowed()
|
||||||
|
.bg(cx.theme().colors().element_disabled)
|
||||||
} else {
|
} else {
|
||||||
el.cursor_pointer()
|
this.bg(style.bg)
|
||||||
|
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.child(match self.label {
|
.child(match self.label {
|
||||||
|
|
|
@ -96,7 +96,7 @@ impl RenderOnce for NumericStepper {
|
||||||
this.overflow_hidden()
|
this.overflow_hidden()
|
||||||
.bg(cx.theme().colors().surface_background)
|
.bg(cx.theme().colors().surface_background)
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
} else {
|
} else {
|
||||||
this.px_1().bg(cx.theme().colors().editor_background)
|
this.px_1().bg(cx.theme().colors().editor_background)
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ impl RenderOnce for NumericStepper {
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||||
.border_r_1()
|
.border_r_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
.child(Icon::new(IconName::Dash).size(IconSize::Small))
|
.child(Icon::new(IconName::Dash).size(IconSize::Small))
|
||||||
.on_click(self.on_decrement),
|
.on_click(self.on_decrement),
|
||||||
)
|
)
|
||||||
|
@ -124,7 +124,6 @@ impl RenderOnce for NumericStepper {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.when(is_outlined, |this| this)
|
|
||||||
.child(Label::new(self.value).mx_3())
|
.child(Label::new(self.value).mx_3())
|
||||||
.map(|increment| {
|
.map(|increment| {
|
||||||
if is_outlined {
|
if is_outlined {
|
||||||
|
@ -136,7 +135,7 @@ impl RenderOnce for NumericStepper {
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||||
.border_l_1()
|
.border_l_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border_variant)
|
||||||
.child(Icon::new(IconName::Plus).size(IconSize::Small))
|
.child(Icon::new(IconName::Plus).size(IconSize::Small))
|
||||||
.on_click(self.on_increment),
|
.on_click(self.on_increment),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue