mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-23 18:32:17 +00:00
Translucent/cropped menu when edit prediction is selected + no aside
Displaying the edit prediction inline in the buffer is better than in an aside, as then the user doesn't need to re-locate the edit within the buffer. Motivation for making the edit menu translucent + cropped + no content is to not obscure the displayed prediction within the buffer, while still making it clear that the menu is open and so pressing `down` will select the first LSP completion in the menu. Release Notes: - N/A
This commit is contained in:
parent
82cee9e9a4
commit
b7c77784a8
4 changed files with 206 additions and 70 deletions
|
@ -1,8 +1,9 @@
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement,
|
div, pulsating_between, px, uniform_list, Animation, AnimationExt, AnyElement,
|
||||||
BackgroundExecutor, Div, FontWeight, ListSizingBehavior, Model, ScrollStrategy, SharedString,
|
BackgroundExecutor, Div, FontWeight, Hsla, ListSizingBehavior, Model, ScrollStrategy,
|
||||||
Size, StrikethroughStyle, StyledText, UniformListScrollHandle, ViewContext, WeakView,
|
SharedString, Size, StrikethroughStyle, StyledText, TextStyleRefinement,
|
||||||
|
UniformListScrollHandle, ViewContext, WeakView,
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use language::{CodeLabel, Documentation};
|
use language::{CodeLabel, Documentation};
|
||||||
|
@ -20,7 +21,7 @@ use std::{
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
};
|
};
|
||||||
use task::ResolvedTask;
|
use task::ResolvedTask;
|
||||||
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, Styled};
|
use ui::{prelude::*, Color, IntoElement, ListItem, Pixels, Popover, PopoverElision, Styled};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ use crate::{
|
||||||
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
|
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
|
||||||
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
|
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
|
||||||
};
|
};
|
||||||
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
|
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint};
|
||||||
|
|
||||||
pub const MENU_GAP: Pixels = px(4.);
|
pub const MENU_GAP: Pixels = px(4.);
|
||||||
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
|
pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
|
||||||
|
@ -320,6 +321,19 @@ impl CompletionsMenu {
|
||||||
self.update_selection_index(index, provider, cx);
|
self.update_selection_index(index, provider, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select_initial_lsp_completion(
|
||||||
|
&mut self,
|
||||||
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
cx: &mut ViewContext<Editor>,
|
||||||
|
) {
|
||||||
|
let index = if self.inline_completion_present() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
self.update_selection_index(index, provider, cx);
|
||||||
|
}
|
||||||
|
|
||||||
fn update_selection_index(
|
fn update_selection_index(
|
||||||
&mut self,
|
&mut self,
|
||||||
match_index: usize,
|
match_index: usize,
|
||||||
|
@ -353,13 +367,10 @@ impl CompletionsMenu {
|
||||||
|
|
||||||
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
|
pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
|
||||||
let hint = CompletionEntry::InlineCompletionHint(hint);
|
let hint = CompletionEntry::InlineCompletionHint(hint);
|
||||||
let mut entries = self.entries.borrow_mut();
|
if self.inline_completion_present() {
|
||||||
match entries.first() {
|
self.entries.borrow_mut()[0] = hint;
|
||||||
Some(CompletionEntry::InlineCompletionHint { .. }) => {
|
} else {
|
||||||
entries[0] = hint;
|
self.entries.borrow_mut().insert(0, hint);
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
entries.insert(0, hint);
|
|
||||||
// When `y_flipped`, need to scroll to bring it into view.
|
// When `y_flipped`, need to scroll to bring it into view.
|
||||||
if self.selected_item == 0 {
|
if self.selected_item == 0 {
|
||||||
self.scroll_handle
|
self.scroll_handle
|
||||||
|
@ -367,6 +378,28 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn inline_completion_present(&self) -> bool {
|
||||||
|
self.entries.borrow().first().map_or(false, |entry| {
|
||||||
|
matches!(entry, CompletionEntry::InlineCompletionHint(_))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inline_completion_selected(&self) -> bool {
|
||||||
|
self.selected_item == 0
|
||||||
|
&& self.entries.borrow().first().map_or(false, |entry| {
|
||||||
|
matches!(entry, CompletionEntry::InlineCompletionHint(_))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inline_completion_selected_and_loaded(&self) -> bool {
|
||||||
|
self.selected_item == 0
|
||||||
|
&& self.entries.borrow().first().map_or(false, |entry| {
|
||||||
|
matches!(
|
||||||
|
entry,
|
||||||
|
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { .. })
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_visible_completions(
|
pub fn resolve_visible_completions(
|
||||||
|
@ -467,7 +500,7 @@ impl CompletionsMenu {
|
||||||
fn render(
|
fn render(
|
||||||
&self,
|
&self,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
max_height_in_lines: u32,
|
mut max_height_in_lines: u32,
|
||||||
y_flipped: bool,
|
y_flipped: bool,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
@ -499,11 +532,25 @@ impl CompletionsMenu {
|
||||||
.map(|(ix, _)| ix);
|
.map(|(ix, _)| ix);
|
||||||
drop(completions);
|
drop(completions);
|
||||||
|
|
||||||
|
let translucent = self.inline_completion_selected_and_loaded();
|
||||||
|
if translucent {
|
||||||
|
max_height_in_lines = max_height_in_lines.min(2);
|
||||||
|
}
|
||||||
|
|
||||||
let selected_item = self.selected_item;
|
let selected_item = self.selected_item;
|
||||||
let completions = self.completions.clone();
|
let completions = self.completions.clone();
|
||||||
let entries = self.entries.clone();
|
let entries = self.entries.clone();
|
||||||
let last_rendered_range = self.last_rendered_range.clone();
|
let last_rendered_range = self.last_rendered_range.clone();
|
||||||
let style = style.clone();
|
let editor_text_style = if translucent {
|
||||||
|
// TODO: Opacity of parent should apply (but doesn't).
|
||||||
|
Rc::new(style.text.clone().refined(TextStyleRefinement {
|
||||||
|
color: Some(Hsla::transparent_black()),
|
||||||
|
..Default::default()
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Rc::new(style.text.clone())
|
||||||
|
};
|
||||||
|
let editor_syntax_theme = style.syntax.clone();
|
||||||
let list = uniform_list(
|
let list = uniform_list(
|
||||||
cx.view().clone(),
|
cx.view().clone(),
|
||||||
"completions",
|
"completions",
|
||||||
|
@ -513,16 +560,24 @@ impl CompletionsMenu {
|
||||||
let start_ix = range.start;
|
let start_ix = range.start;
|
||||||
let completions_guard = completions.borrow_mut();
|
let completions_guard = completions.borrow_mut();
|
||||||
|
|
||||||
|
let editor_text_style = editor_text_style.clone();
|
||||||
|
let editor_syntax_theme = editor_syntax_theme.clone();
|
||||||
entries.borrow()[range]
|
entries.borrow()[range]
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, mat)| {
|
.map(move |(ix, mat)| {
|
||||||
let item_ix = start_ix + ix;
|
let item_ix = start_ix + ix;
|
||||||
let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone();
|
let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone();
|
||||||
let base_label = h_flex()
|
let base_label = h_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(div().font(buffer_font.clone()).child("Zed AI"))
|
.child(div().font(buffer_font.clone()).child("Zed AI"))
|
||||||
.child(div().px_0p5().child("/").opacity(0.2));
|
.child(div().px_0p5().child("/").opacity(if translucent {
|
||||||
|
// TODO: Opacity of parent should apply (but doesn't).
|
||||||
|
0.
|
||||||
|
} else {
|
||||||
|
0.2
|
||||||
|
}))
|
||||||
|
.when(translucent, |this| this.opacity(0.));
|
||||||
|
|
||||||
match mat {
|
match mat {
|
||||||
CompletionEntry::Match(mat) => {
|
CompletionEntry::Match(mat) => {
|
||||||
|
@ -543,8 +598,12 @@ impl CompletionsMenu {
|
||||||
FontWeight::BOLD.into(),
|
FontWeight::BOLD.into(),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
styled_runs_for_code_label(&completion.label, &style.syntax)
|
styled_runs_for_code_label(
|
||||||
.map(|(range, mut highlight)| {
|
&completion.label,
|
||||||
|
&editor_syntax_theme,
|
||||||
|
)
|
||||||
|
.map(
|
||||||
|
|(range, mut highlight)| {
|
||||||
// Ignore font weight for syntax highlighting, as we'll use it
|
// Ignore font weight for syntax highlighting, as we'll use it
|
||||||
// for fuzzy matches.
|
// for fuzzy matches.
|
||||||
highlight.font_weight = None;
|
highlight.font_weight = None;
|
||||||
|
@ -561,39 +620,55 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
(range, highlight)
|
(range, highlight)
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
let completion_label =
|
let completion_label =
|
||||||
|
div().when(translucent, |this| this.opacity(0.)).child(
|
||||||
StyledText::new(completion.label.text.clone())
|
StyledText::new(completion.label.text.clone())
|
||||||
.with_highlights(&style.text, highlights);
|
.with_highlights(&editor_text_style, highlights),
|
||||||
|
);
|
||||||
let documentation_label =
|
let documentation_label =
|
||||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||||
if text.trim().is_empty() {
|
if text.trim().is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(
|
Some(
|
||||||
|
div()
|
||||||
|
.when(translucent, |this| this.opacity(0.))
|
||||||
|
.child(
|
||||||
Label::new(text.clone())
|
Label::new(text.clone())
|
||||||
.ml_4()
|
.ml_4()
|
||||||
.size(LabelSize::Small)
|
.size(LabelSize::Small)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let color_swatch = completion
|
let color_swatch = completion.color().map(|color| {
|
||||||
.color()
|
div()
|
||||||
.map(|color| div().size_4().bg(color).rounded_sm());
|
.size_4()
|
||||||
|
.rounded_sm()
|
||||||
|
.when(!translucent, |this| this.bg(color))
|
||||||
|
});
|
||||||
|
|
||||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
div().min_w(px(220.)).max_w(px(540.)).child(
|
||||||
ListItem::new(mat.candidate_id)
|
ListItem::new(mat.candidate_id)
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(item_ix == selected_item)
|
.toggle_state(item_ix == selected_item)
|
||||||
|
// TODO: Ideally text would show on mouse hover indicating
|
||||||
|
// that clicking will cause lsp completions to be shown.
|
||||||
|
.when(translucent, |this| this.opacity(0.2))
|
||||||
.on_click(cx.listener(move |editor, _event, cx| {
|
.on_click(cx.listener(move |editor, _event, cx| {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
if let Some(task) = editor.confirm_completion(
|
if translucent {
|
||||||
|
editor
|
||||||
|
.context_menu_select_initial_lsp_completion(cx);
|
||||||
|
} else if let Some(task) = editor.confirm_completion(
|
||||||
&ConfirmCompletion {
|
&ConfirmCompletion {
|
||||||
item_ix: Some(item_ix),
|
item_ix: Some(item_ix),
|
||||||
},
|
},
|
||||||
|
@ -604,7 +679,7 @@ impl CompletionsMenu {
|
||||||
}))
|
}))
|
||||||
.start_slot::<Div>(color_swatch)
|
.start_slot::<Div>(color_swatch)
|
||||||
.child(h_flex().overflow_hidden().child(completion_label))
|
.child(h_flex().overflow_hidden().child(completion_label))
|
||||||
.end_slot::<Label>(documentation_label),
|
.end_slot::<Div>(documentation_label),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CompletionEntry::InlineCompletionHint(
|
CompletionEntry::InlineCompletionHint(
|
||||||
|
@ -617,7 +692,7 @@ impl CompletionsMenu {
|
||||||
.child(
|
.child(
|
||||||
base_label.child(
|
base_label.child(
|
||||||
StyledText::new(hint.label())
|
StyledText::new(hint.label())
|
||||||
.with_highlights(&style.text, None),
|
.with_highlights(&editor_text_style, None),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -629,19 +704,23 @@ impl CompletionsMenu {
|
||||||
.toggle_state(item_ix == selected_item)
|
.toggle_state(item_ix == selected_item)
|
||||||
.start_slot(Icon::new(IconName::ZedPredict))
|
.start_slot(Icon::new(IconName::ZedPredict))
|
||||||
.child(base_label.child({
|
.child(base_label.child({
|
||||||
let text_style = style.text.clone();
|
|
||||||
StyledText::new(hint.label())
|
StyledText::new(hint.label())
|
||||||
.with_highlights(&text_style, None)
|
.with_highlights(&editor_text_style, None)
|
||||||
.with_animation(
|
.with_animation(
|
||||||
"pulsating-label",
|
"pulsating-label",
|
||||||
Animation::new(Duration::from_secs(1))
|
Animation::new(Duration::from_secs(1))
|
||||||
.repeat()
|
.repeat()
|
||||||
.with_easing(pulsating_between(0.4, 0.8)),
|
.with_easing(pulsating_between(0.4, 0.8)),
|
||||||
|
{
|
||||||
|
let editor_text_style =
|
||||||
|
editor_text_style.clone();
|
||||||
move |text, delta| {
|
move |text, delta| {
|
||||||
let mut text_style = text_style.clone();
|
let mut text_style =
|
||||||
|
(*editor_text_style).clone();
|
||||||
text_style.color =
|
text_style.color =
|
||||||
text_style.color.opacity(delta);
|
text_style.color.opacity(delta);
|
||||||
text.with_highlights(&text_style, None)
|
text.with_highlights(&text_style, None)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})),
|
})),
|
||||||
|
@ -656,7 +735,7 @@ impl CompletionsMenu {
|
||||||
.child(
|
.child(
|
||||||
base_label.child(
|
base_label.child(
|
||||||
StyledText::new(hint.label())
|
StyledText::new(hint.label())
|
||||||
.with_highlights(&style.text, None),
|
.with_highlights(&editor_text_style, None),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.on_click(cx.listener(move |editor, _event, cx| {
|
.on_click(cx.listener(move |editor, _event, cx| {
|
||||||
|
@ -664,18 +743,23 @@ impl CompletionsMenu {
|
||||||
editor.toggle_zed_predict_tos(cx);
|
editor.toggle_zed_predict_tos(cx);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|
||||||
CompletionEntry::InlineCompletionHint(
|
CompletionEntry::InlineCompletionHint(
|
||||||
hint @ InlineCompletionMenuHint::Loaded { .. },
|
hint @ InlineCompletionMenuHint::Loaded { .. },
|
||||||
) => div().min_w(px(250.)).max_w(px(500.)).child(
|
) => div().min_w(px(250.)).max_w(px(500.)).child(
|
||||||
ListItem::new("inline-completion")
|
ListItem::new("inline-completion")
|
||||||
.inset(true)
|
.inset(true)
|
||||||
.toggle_state(item_ix == selected_item)
|
.toggle_state(item_ix == selected_item)
|
||||||
.start_slot(Icon::new(IconName::ZedPredict))
|
// TODO: Ideally the contents would display on hover.
|
||||||
|
.when(translucent, |this| this.opacity(0.2))
|
||||||
|
.start_slot(
|
||||||
|
div()
|
||||||
|
.child(Icon::new(IconName::ZedPredict))
|
||||||
|
.when(translucent, |this| this.opacity(0.)),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
base_label.child(
|
base_label.child(
|
||||||
StyledText::new(hint.label())
|
StyledText::new(hint.label())
|
||||||
.with_highlights(&style.text, None),
|
.with_highlights(&editor_text_style, None),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.on_click(cx.listener(move |editor, _event, cx| {
|
.on_click(cx.listener(move |editor, _event, cx| {
|
||||||
|
@ -698,7 +782,20 @@ impl CompletionsMenu {
|
||||||
.with_width_from_item(widest_completion_ix)
|
.with_width_from_item(widest_completion_ix)
|
||||||
.with_sizing_behavior(ListSizingBehavior::Infer);
|
.with_sizing_behavior(ListSizingBehavior::Infer);
|
||||||
|
|
||||||
Popover::new().child(list).into_any_element()
|
let elision = if translucent {
|
||||||
|
if self.scroll_handle.y_flipped() {
|
||||||
|
PopoverElision::TranslucentWithCroppedTop
|
||||||
|
} else {
|
||||||
|
PopoverElision::TranslucentWithCroppedBottom
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PopoverElision::None
|
||||||
|
};
|
||||||
|
|
||||||
|
Popover::new()
|
||||||
|
.elision(elision)
|
||||||
|
.child(list)
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_aside(
|
fn render_aside(
|
||||||
|
@ -734,19 +831,6 @@ impl CompletionsMenu {
|
||||||
Documentation::Undocumented => return None,
|
Documentation::Undocumented => return None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => {
|
|
||||||
match text {
|
|
||||||
InlineCompletionText::Edit { text, highlights } => div()
|
|
||||||
.mx_1()
|
|
||||||
.rounded_md()
|
|
||||||
.bg(cx.theme().colors().editor_background)
|
|
||||||
.child(
|
|
||||||
gpui::StyledText::new(text.clone())
|
|
||||||
.with_highlights(&style.text, highlights.clone()),
|
|
||||||
),
|
|
||||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CompletionEntry::InlineCompletionHint(_) => return None,
|
CompletionEntry::InlineCompletionHint(_) => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -766,10 +850,7 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
|
||||||
let inline_completion_was_selected = self.selected_item == 0
|
let inline_completion_was_selected = self.inline_completion_selected();
|
||||||
&& self.entries.borrow().first().map_or(false, |entry| {
|
|
||||||
matches!(entry, CompletionEntry::InlineCompletionHint(_))
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut matches = if let Some(query) = query {
|
let mut matches = if let Some(query) = query {
|
||||||
fuzzy::match_strings(
|
fuzzy::match_strings(
|
||||||
|
|
|
@ -7548,6 +7548,18 @@ impl Editor {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn context_menu_select_initial_lsp_completion(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
|
||||||
|
match context_menu {
|
||||||
|
CodeContextMenu::Completions(completions_menu) => {
|
||||||
|
completions_menu
|
||||||
|
.select_initial_lsp_completion(self.completion_provider.as_deref(), cx);
|
||||||
|
}
|
||||||
|
CodeContextMenu::CodeActions(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
|
pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
|
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {
|
||||||
context_menu.select_first(self.completion_provider.as_deref(), cx);
|
context_menu.select_first(self.completion_provider.as_deref(), cx);
|
||||||
|
|
|
@ -42,6 +42,7 @@ pub struct ListItem {
|
||||||
outlined: bool,
|
outlined: bool,
|
||||||
overflow_x: bool,
|
overflow_x: bool,
|
||||||
focused: Option<bool>,
|
focused: Option<bool>,
|
||||||
|
opacity: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem {
|
impl ListItem {
|
||||||
|
@ -67,6 +68,7 @@ impl ListItem {
|
||||||
outlined: false,
|
outlined: false,
|
||||||
overflow_x: false,
|
overflow_x: false,
|
||||||
focused: None,
|
focused: None,
|
||||||
|
opacity: 1.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +157,11 @@ impl ListItem {
|
||||||
self.focused = Some(focused);
|
self.focused = Some(focused);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn opacity(mut self, opacity: f32) -> Self {
|
||||||
|
self.opacity = opacity;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Disableable for ListItem {
|
impl Disableable for ListItem {
|
||||||
|
@ -183,6 +190,7 @@ impl RenderOnce for ListItem {
|
||||||
.id(self.id)
|
.id(self.id)
|
||||||
.w_full()
|
.w_full()
|
||||||
.relative()
|
.relative()
|
||||||
|
.opacity(self.opacity)
|
||||||
// When an item is inset draw the indent spacing outside of the item
|
// When an item is inset draw the indent spacing outside of the item
|
||||||
.when(self.inset, |this| {
|
.when(self.inset, |this| {
|
||||||
this.ml(self.indent_level as f32 * self.indent_step_size)
|
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::v_flex;
|
use crate::v_flex;
|
||||||
|
use crate::ElevationIndex;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, AnyElement, Element, IntoElement, ParentElement, Pixels, RenderOnce, Styled, WindowContext,
|
div, AnyElement, Element, IntoElement, ParentElement, Pixels, RenderOnce, Styled, WindowContext,
|
||||||
};
|
};
|
||||||
|
@ -41,19 +42,44 @@ pub const POPOVER_Y_PADDING: Pixels = px(8.);
|
||||||
pub struct Popover {
|
pub struct Popover {
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
aside: Option<AnyElement>,
|
aside: Option<AnyElement>,
|
||||||
|
elision: PopoverElision,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PopoverElision {
|
||||||
|
None,
|
||||||
|
TranslucentWithCroppedTop,
|
||||||
|
TranslucentWithCroppedBottom,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for Popover {
|
impl RenderOnce for Popover {
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
let inner = v_flex()
|
||||||
|
.rounded_lg()
|
||||||
|
.border_1()
|
||||||
|
.py(POPOVER_Y_PADDING / 2.)
|
||||||
|
.children(self.children);
|
||||||
|
let inner = match self.elision {
|
||||||
|
PopoverElision::None => inner
|
||||||
|
.bg(cx.theme().colors().elevated_surface_background)
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.shadow(ElevationIndex::ElevatedSurface.shadow()),
|
||||||
|
PopoverElision::TranslucentWithCroppedBottom => inner
|
||||||
|
.bg(cx.theme().colors().elevated_surface_background.opacity(0.5))
|
||||||
|
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||||
|
.rounded_bl_none()
|
||||||
|
.rounded_br_none()
|
||||||
|
.border_b(px(0.)),
|
||||||
|
PopoverElision::TranslucentWithCroppedTop => inner
|
||||||
|
.bg(cx.theme().colors().elevated_surface_background.opacity(0.5))
|
||||||
|
.border_color(cx.theme().colors().border_variant.opacity(0.5))
|
||||||
|
.rounded_tl_none()
|
||||||
|
.rounded_tr_none()
|
||||||
|
.border_t(px(0.)),
|
||||||
|
};
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(inner)
|
||||||
v_flex()
|
|
||||||
.elevation_2(cx)
|
|
||||||
.py(POPOVER_Y_PADDING / 2.)
|
|
||||||
.children(self.children),
|
|
||||||
)
|
|
||||||
.when_some(self.aside, |this, aside| {
|
.when_some(self.aside, |this, aside| {
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
@ -77,6 +103,7 @@ impl Popover {
|
||||||
Self {
|
Self {
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
aside: None,
|
aside: None,
|
||||||
|
elision: PopoverElision::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +114,14 @@ impl Popover {
|
||||||
self.aside = Some(aside.into_element().into_any());
|
self.aside = Some(aside.into_element().into_any());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn elision(mut self, elision: PopoverElision) -> Self
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
self.elision = elision;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParentElement for Popover {
|
impl ParentElement for Popover {
|
||||||
|
|
Loading…
Reference in a new issue