diff --git a/lapce-data/src/command.rs b/lapce-data/src/command.rs index 7df3134d..367bbaf8 100644 --- a/lapce-data/src/command.rs +++ b/lapce-data/src/command.rs @@ -50,13 +50,13 @@ pub const LAPCE_UI_COMMAND: Selector = Selector::new("lapce.ui_command"); -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct LapceCommand { pub kind: CommandKind, pub data: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum CommandKind { Workbench(LapceWorkbenchCommand), Edit(EditCommand), @@ -544,8 +544,8 @@ pub enum LapceUICommand { RunPaletteReferences(Vec>), InitPaletteInput(String), UpdatePaletteInput(String), - UpdatePaletteItems(String, Vec), - FilterPaletteItems(String, String, Vec), + UpdatePaletteItems(String, im::Vector), + FilterPaletteItems(String, String, im::Vector), UpdateKeymapsFilter(String), ResetSettingsFile(String, String), UpdateSettingsFile(String, String, Value), @@ -712,6 +712,10 @@ pub enum LapceUICommand { }, FileExplorerRefresh, SetLanguage(String), + + /// An item in a list was chosen + /// This is typically targeted at the widget which contains the list + ListItemSelected, } /// This can't be an `FnOnce` because we only ever get a reference to diff --git a/lapce-data/src/completion.rs b/lapce-data/src/completion.rs index 0c071b38..e5469c0a 100644 --- a/lapce-data/src/completion.rs +++ b/lapce-data/src/completion.rs @@ -1,16 +1,16 @@ use std::{fmt::Display, path::PathBuf, sync::Arc}; use anyhow::Error; -use druid::{Size, WidgetId}; +use druid::{EventCtx, Size, WidgetId}; use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use itertools::Itertools; -use lapce_core::movement::Movement; +use lapce_core::command::FocusCommand; use lapce_rpc::{buffer::BufferId, plugin::PluginId}; use lsp_types::{CompletionItem, CompletionResponse, Position}; use regex::Regex; use std::str::FromStr; -use crate::proxy::LapceProxy; +use crate::{config::Config, list::ListData, proxy::LapceProxy}; #[derive(Debug)] pub struct Snippet { @@ -240,94 +240,70 @@ pub struct CompletionData { pub offset: usize, pub buffer_id: BufferId, pub input: String, - pub index: usize, - pub input_items: im::HashMap>>, - empty: Arc>, - pub filtered_items: Arc>, + pub input_items: im::HashMap>, + empty: im::Vector, + pub completion_list: ListData, pub matcher: Arc, - /// The size of the completion list - pub size: Size, /// The size of the documentation view pub documentation_size: Size, } impl CompletionData { - pub fn new() -> Self { + pub fn new(config: Arc) -> Self { + let id = WidgetId::next(); + let mut completion_list = ListData::new(config, id, ()); + // TODO: Make this configurable + completion_list.max_displayed_items = 15; Self { - id: WidgetId::next(), + id, scroll_id: WidgetId::next(), documentation_scroll_id: WidgetId::next(), request_id: 0, - index: 0, offset: 0, status: CompletionStatus::Inactive, buffer_id: BufferId(0), input: "".to_string(), input_items: im::HashMap::new(), - filtered_items: Arc::new(Vec::new()), + completion_list, matcher: Arc::new(SkimMatcherV2::default().ignore_case()), - size: Size::new(400.0, 300.0), // TODO: Make this configurable documentation_size: Size::new(400.0, 300.0), - empty: Arc::new(Vec::new()), + empty: im::Vector::new(), } } + /// Return the number of entries that are displayable pub fn len(&self) -> usize { - self.current_items().len() + self.completion_list.items.len() } pub fn is_empty(&self) -> bool { self.len() == 0 } - /// We need the line height so that we can get the number displayed as a number, since - /// we just render as many fit inside the `size` defined for completion. - fn entry_count(&self, editor_line_height: usize) -> usize { - ((self.size.height / editor_line_height as f64).ceil() as usize) - .saturating_sub(1) - } - - pub fn next(&mut self) { - self.index = Movement::Down.update_index(self.index, self.len(), 1, true); - } - - pub fn next_page(&mut self, editor_line_height: usize) { - let count = self.entry_count(editor_line_height); - self.index = - Movement::Down.update_index(self.index, self.len(), count, false); - } - - pub fn previous(&mut self) { - self.index = Movement::Up.update_index(self.index, self.len(), 1, true); - } - - pub fn previous_page(&mut self, editor_line_height: usize) { - let count = self.entry_count(editor_line_height); - self.index = Movement::Up.update_index(self.index, self.len(), count, false); - } - - pub fn current_items(&self) -> &Arc> { + pub fn current_items(&self) -> &im::Vector { if self.input.is_empty() { self.all_items() } else { - &self.filtered_items + &self.completion_list.items } } - pub fn all_items(&self) -> &Arc> { + pub fn all_items(&self) -> &im::Vector { self.input_items .get(&self.input) .filter(|items| !items.is_empty()) .unwrap_or_else(move || self.input_items.get("").unwrap_or(&self.empty)) } - pub fn current_item(&self) -> &ScoredCompletionItem { - &self.current_items()[self.index] + pub fn current_item(&self) -> Option<&ScoredCompletionItem> { + self.completion_list + .items + .get(self.completion_list.selected_index) } - pub fn current(&self) -> &str { - self.current_items()[self.index].item.label.as_str() + pub fn current(&self) -> Option<&str> { + self.current_item().map(|item| item.item.label.as_str()) } #[allow(clippy::too_many_arguments)] @@ -351,12 +327,12 @@ pub fn cancel(&mut self) { self.status = CompletionStatus::Inactive; self.input = "".to_string(); self.input_items.clear(); - self.index = 0; + self.completion_list.clear_items(); } pub fn update_input(&mut self, input: String) { self.input = input; - self.index = 0; + self.completion_list.selected_index = 0; if self.status == CompletionStatus::Inactive { return; } @@ -379,7 +355,7 @@ pub fn receive( CompletionResponse::Array(items) => items, CompletionResponse::List(list) => list.items, }; - let items: Vec = items + let items: im::Vector = items .iter() .map(|i| ScoredCompletionItem { item: i.to_owned(), @@ -390,20 +366,21 @@ pub fn receive( }) .collect(); - self.input_items.insert(input, Arc::new(items)); + self.input_items.insert(input, items); self.filter_items(); - if self.index >= self.len() { - self.index = 0; + if self.completion_list.selected_index >= self.len() { + self.completion_list.selected_index = 0; } } pub fn filter_items(&mut self) { if self.input.is_empty() { + self.completion_list.items = self.all_items().clone(); return; } - let mut items: Vec = self + let mut items: im::Vector = self .all_items() .iter() .filter_map(|i| { @@ -439,31 +416,15 @@ pub fn filter_items(&mut self) { .then_with(|| b.label_score.cmp(&a.label_score)) .then_with(|| a.item.label.len().cmp(&b.item.label.len())) }); - self.filtered_items = Arc::new(items); + self.completion_list.items = items; + } + + pub fn run_focus_command(&mut self, ctx: &mut EventCtx, command: &FocusCommand) { + self.completion_list.run_focus_command(ctx, command); } } -impl Default for CompletionData { - fn default() -> Self { - Self::new() - } -} - -pub struct Completion {} - -impl Completion { - pub fn new() -> Self { - Self {} - } -} - -impl Default for Completion { - fn default() -> Self { - Self::new() - } -} - -#[derive(Clone)] +#[derive(Clone, PartialEq)] pub struct ScoredCompletionItem { pub item: CompletionItem, pub plugin_id: PluginId, diff --git a/lapce-data/src/data.rs b/lapce-data/src/data.rs index 14df4f57..7eaf040c 100644 --- a/lapce-data/src/data.rs +++ b/lapce-data/src/data.rs @@ -664,8 +664,8 @@ pub fn new( term_sender.clone(), event_sink.clone(), )); - let palette = Arc::new(PaletteData::new(proxy.clone())); - let completion = Arc::new(CompletionData::new()); + let palette = Arc::new(PaletteData::new(config.clone(), proxy.clone())); + let completion = Arc::new(CompletionData::new(config.clone())); let hover = Arc::new(HoverData::new()); let rename = Arc::new(RenameData::new()); let source_control = Arc::new(SourceControlData::new()); @@ -958,6 +958,7 @@ pub fn completion_origin( &self, text: &mut PietText, tab_size: Size, + completion_size: Size, config: &Config, ) -> Point { let line_height = self.config.editor.line_height() as f64; @@ -986,10 +987,8 @@ pub fn completion_origin( let mut origin = *editor.window_origin.borrow() - self.window_origin.borrow().to_vec2() + Vec2::new(point_below.x - line_height - 5.0, point_below.y); - if origin.y + self.completion.size.height + 1.0 > tab_size.height { - let height = self - .completion - .size + if origin.y + completion_size.height + 1.0 > tab_size.height { + let height = completion_size .height .min(self.completion.len() as f64 * line_height); origin.y = editor.window_origin.borrow().y @@ -997,8 +996,8 @@ pub fn completion_origin( + point_above.y - height; } - if origin.x + self.completion.size.width + 1.0 > tab_size.width { - origin.x = tab_size.width - self.completion.size.width - 1.0; + if origin.x + completion_size.width + 1.0 > tab_size.width { + origin.x = tab_size.width - completion_size.width - 1.0; } if origin.x <= 0.0 { origin.x = 0.0; diff --git a/lapce-data/src/editor.rs b/lapce-data/src/editor.rs index 4fde632a..e66bf226 100644 --- a/lapce-data/src/editor.rs +++ b/lapce-data/src/editor.rs @@ -196,7 +196,7 @@ fn init_buffer_content_cmd( } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct EditorLocation { pub path: PathBuf, pub position: Option

, @@ -632,6 +632,52 @@ pub fn cancel_rename(&mut self, ctx: &mut EventCtx) { } } + pub fn completion_item_select(&mut self, ctx: &mut EventCtx) { + let item = if let Some(item) = self.completion.current_item() { + item.to_owned() + } else { + // There was no selected item, this may be due to a bug in failing to ensure that the index was valid. + return; + }; + + self.cancel_completion(); + if item.item.data.is_some() { + let view_id = self.editor.view_id; + let buffer_id = self.doc.id(); + let rev = self.doc.rev(); + let offset = self.editor.cursor.offset(); + let event_sink = ctx.get_external_handle(); + self.proxy.proxy_rpc.completion_resolve( + item.plugin_id, + item.item.clone(), + move |result| { + // let item = result.unwrap_or_else(|_| item.clone()); + let item = + if let Ok(ProxyResponse::CompletionResolveResponse { + item, + }) = result + { + *item + } else { + item.item.clone() + }; + let _ = event_sink.submit_command( + LAPCE_UI_COMMAND, + LapceUICommand::ResolveCompletion( + buffer_id, + rev, + offset, + Box::new(item), + ), + Target::Widget(view_id), + ); + }, + ); + } else { + let _ = self.apply_completion_item(&item.item); + } + } + /// Update the displayed autocompletion box /// Sends a request to the LSP for completion information fn update_completion( @@ -1679,44 +1725,8 @@ fn run_focus_command( Target::Widget(self.palette.widget_id), )); } else { - let item = self.completion.current_item().to_owned(); - self.cancel_completion(); - if item.item.data.is_some() { - let view_id = self.editor.view_id; - let buffer_id = self.doc.id(); - let rev = self.doc.rev(); - let offset = self.editor.cursor.offset(); - let event_sink = ctx.get_external_handle(); - self.proxy.proxy_rpc.completion_resolve( - item.plugin_id, - item.item.clone(), - move |result| { - // let item = result.unwrap_or_else(|_| item.clone()); - let item = if let Ok( - ProxyResponse::CompletionResolveResponse { - item, - }, - ) = result - { - *item - } else { - item.item.clone() - }; - let _ = event_sink.submit_command( - LAPCE_UI_COMMAND, - LapceUICommand::ResolveCompletion( - buffer_id, - rev, - offset, - Box::new(item), - ), - Target::Widget(view_id), - ); - }, - ); - } else { - let _ = self.apply_completion_item(&item.item); - } + let completion = Arc::make_mut(&mut self.completion); + completion.run_focus_command(ctx, cmd); } } ListNext => { @@ -1731,7 +1741,7 @@ fn run_focus_command( )); } else { let completion = Arc::make_mut(&mut self.completion); - completion.next(); + completion.run_focus_command(ctx, cmd); } } ListNextPage => { @@ -1746,7 +1756,7 @@ fn run_focus_command( )); } else { let completion = Arc::make_mut(&mut self.completion); - completion.next_page(self.config.editor.line_height()); + completion.run_focus_command(ctx, cmd); } } ListPrevious => { @@ -1761,7 +1771,7 @@ fn run_focus_command( )); } else { let completion = Arc::make_mut(&mut self.completion); - completion.previous(); + completion.run_focus_command(ctx, cmd); } } ListPreviousPage => { @@ -1776,7 +1786,7 @@ fn run_focus_command( )); } else { let completion = Arc::make_mut(&mut self.completion); - completion.previous_page(self.config.editor.line_height()); + completion.run_focus_command(ctx, cmd); } } JumpToNextSnippetPlaceholder => { diff --git a/lapce-data/src/lib.rs b/lapce-data/src/lib.rs index b07a9e27..9a9d3528 100644 --- a/lapce-data/src/lib.rs +++ b/lapce-data/src/lib.rs @@ -13,6 +13,7 @@ pub mod history; pub mod hover; pub mod keypress; +pub mod list; pub mod markdown; pub mod menu; pub mod palette; diff --git a/lapce-data/src/list.rs b/lapce-data/src/list.rs new file mode 100644 index 00000000..7f9b43d0 --- /dev/null +++ b/lapce-data/src/list.rs @@ -0,0 +1,203 @@ +use std::sync::Arc; + +use druid::{Command, Data, EventCtx, Target, WidgetId}; +use lapce_core::{command::FocusCommand, movement::Movement}; + +use crate::{ + command::{ + CommandExecuted, CommandKind, LapceCommand, LapceUICommand, LAPCE_UI_COMMAND, + }, + config::{Config, GetConfig}, +}; + +// Note: when adding fields to this, make sure to think whether they need to be added to the `same` +// implementation +/// Note: all `T` are going to be required by the UI code to implement `ListPaint` +#[derive(Clone)] +pub struct ListData { + /// The id of the widget that contains the [`ListData`] which wishes to receive + /// events (such as when an entry is selected) + pub parent: WidgetId, + + /// The items that can be selected from the list + /// Note that since this is an `im::Vector`, cloning is cheap. + pub items: im::Vector, + /// Extra data attached to the list for the item rendering to use. + pub data: D, + + /// The index of the item which is selected + pub selected_index: usize, + + /// The maximum number of items to render in the list. + pub max_displayed_items: usize, + + /// The line height of each list element + /// Defaults to the editor line height if not set + pub line_height: Option, + + // These should be filled whenever you call into the `List` widget + pub config: Arc, +} +impl ListData { + pub fn new( + config: Arc, + parent: WidgetId, + held_data: D, + ) -> ListData { + ListData { + parent, + items: im::Vector::new(), + data: held_data, + selected_index: 0, + max_displayed_items: 15, + line_height: None, + config, + } + } + + /// Clone the list data, giving it data needed to update it + /// This is typically what you need to use to ensure that it has the + /// appropriately updated data when passing the data to the list's widget functions + /// Note that due to the usage of `Arc` and `im::Vector`, cloning is relatively cheap. + pub fn clone_with(&self, config: Arc) -> ListData { + let mut data = self.clone(); + data.update_data(config); + + data + } + + pub fn update_data(&mut self, config: Arc) { + self.config = config; + } + + pub fn line_height(&self) -> usize { + self.line_height + .unwrap_or_else(|| self.config.editor.line_height()) + } + + /// The maximum number of items in the list that can be displayed + /// This is limited by `max_displayed_items` *or* by the number of items + pub fn max_display_count(&self) -> usize { + let mut count = 0; + for _ in self.items.iter() { + count += 1; + if count >= self.max_displayed_items { + return self.max_displayed_items; + } + } + + count + } + + pub fn clear_items(&mut self) { + self.items.clear(); + self.selected_index = 0; + } + + /// Run a command, like those received from KeyPressFocus + pub fn run_command( + &mut self, + ctx: &mut EventCtx, + command: &LapceCommand, + ) -> CommandExecuted { + match &command.kind { + CommandKind::Focus(cmd) => self.run_focus_command(ctx, cmd), + _ => CommandExecuted::No, + } + } + + pub fn run_focus_command( + &mut self, + ctx: &mut EventCtx, + command: &FocusCommand, + ) -> CommandExecuted { + match command { + // ModalClose should be handled (if desired) by the containing widget + FocusCommand::ListNext => { + self.next(); + } + FocusCommand::ListNextPage => { + self.next_page(); + } + FocusCommand::ListPrevious => { + self.previous(); + } + FocusCommand::ListPreviousPage => { + self.previous_page(); + } + FocusCommand::ListSelect => { + self.select(ctx); + } + _ => return CommandExecuted::No, + } + CommandExecuted::Yes + } + + // TODO: Option for whether moving should be wrapping + + pub fn next(&mut self) { + self.selected_index = Movement::Down.update_index( + self.selected_index, + self.items.len(), + 1, + true, + ); + } + + pub fn next_page(&mut self) { + self.selected_index = Movement::Down.update_index( + self.selected_index, + self.items.len(), + self.max_displayed_items - 1, + false, + ); + } + + pub fn previous(&mut self) { + self.selected_index = Movement::Up.update_index( + self.selected_index, + self.items.len(), + 1, + true, + ); + } + + pub fn previous_page(&mut self) { + self.selected_index = Movement::Up.update_index( + self.selected_index, + self.items.len(), + self.max_displayed_items - 1, + false, + ); + } + + pub fn select(&self, ctx: &mut EventCtx) { + ctx.submit_command(Command::new( + LAPCE_UI_COMMAND, + LapceUICommand::ListItemSelected, + Target::Widget(self.parent), + )); + } + + pub fn current_selected_item(&self) -> Option<&T> { + self.items.get(self.selected_index) + } +} +impl Data for ListData { + fn same(&self, other: &Self) -> bool { + // We don't compare the held Config, because that should be updated whenever + // the widget is used + + self.parent == other.parent + && self.items == other.items + && self.data.same(&other.data) + && self.selected_index.same(&other.selected_index) + && self.max_displayed_items.same(&other.max_displayed_items) + && self.line_height.same(&other.line_height) + } +} +impl GetConfig for ListData { + fn get_config(&self) -> &Config { + &self.config + } +} diff --git a/lapce-data/src/palette.rs b/lapce-data/src/palette.rs index 29ea480f..af64a254 100644 --- a/lapce-data/src/palette.rs +++ b/lapce-data/src/palette.rs @@ -9,7 +9,6 @@ use lapce_core::command::{EditCommand, FocusCommand}; use lapce_core::language::LapceLanguage; use lapce_core::mode::Mode; -use lapce_core::movement::Movement; use lapce_rpc::proxy::ProxyResponse; use lsp_types::{DocumentSymbolResponse, Position, Range, SymbolKind}; use std::cmp::Ordering; @@ -22,6 +21,7 @@ use crate::data::{LapceWorkspace, LapceWorkspaceType}; use crate::document::BufferContent; use crate::editor::EditorLocation; +use crate::list::ListData; use crate::panel::PanelKind; use crate::proxy::path_from_url; use crate::{ @@ -119,7 +119,7 @@ pub enum PaletteStatus { Done, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum PaletteItemContent { File(PathBuf, PathBuf), Line(usize, String), @@ -284,7 +284,7 @@ fn select( } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct PaletteItem { pub content: PaletteItemContent, pub filter_text: String, @@ -332,22 +332,31 @@ fn with_mut V>( } } +/// Data to be held by the palette list +#[derive(Data, Clone)] +pub struct PaletteListData { + /// Should only be `None` when it hasn't been updated initially + /// We need this just for some rendering, and not editing it. + pub workspace: Option>, +} + #[derive(Clone)] pub struct PaletteData { pub widget_id: WidgetId, pub scroll_id: WidgetId, pub status: PaletteStatus, + /// Holds information about the list, including the filtered items + pub list_data: ListData, pub proxy: Arc, pub palette_type: PaletteType, - pub sender: Sender<(String, String, Vec)>, - pub receiver: Option)>>, + pub sender: Sender<(String, String, im::Vector)>, + pub receiver: Option)>>, pub run_id: String, pub input: String, pub cursor: usize, - pub index: usize, pub has_nonzero_default_index: bool, - pub items: Vec, - pub filtered_items: Vec, + /// The unfiltered items list + pub total_items: im::Vector, pub preview_editor: WidgetId, pub input_editor: WidgetId, } @@ -361,38 +370,6 @@ fn check_condition(&self, condition: &str) -> bool { matches!(condition, "list_focus" | "palette_focus" | "modal_focus") } - // fn run_command( - // &mut self, - // ctx: &mut EventCtx, - // command: &LapceCommand, - // _count: Option, - // _mods: Modifiers, - // _env: &Env, - // ) -> CommandExecuted { - // match command { - // LapceCommand::ModalClose => { - // self.cancel(ctx); - // } - // LapceCommand::DeleteBackward => { - // self.delete_backward(ctx); - // } - // LapceCommand::DeleteToBeginningOfLine => { - // self.delete_to_beginning_of_line(ctx); - // } - // LapceCommand::ListNext => { - // self.next(ctx); - // } - // LapceCommand::ListPrevious => { - // self.previous(ctx); - // } - // LapceCommand::ListSelect => { - // self.select(ctx); - // } - // _ => return CommandExecuted::No, - // } - // CommandExecuted::Yes - // } - fn receive_char(&mut self, ctx: &mut EventCtx, c: &str) { let palette = Arc::make_mut(&mut self.palette); palette.input.insert_str(palette.cursor, c); @@ -408,28 +385,22 @@ fn run_command( _mods: Modifiers, _env: &Env, ) -> CommandExecuted { + let selected_index = self.palette.list_data.selected_index; + + // Pass any commands, like list movement, to the selector list + Arc::make_mut(&mut self.palette) + .list_data + .run_command(ctx, command); + + // If the selection changed, then update the preview + if selected_index != self.palette.list_data.selected_index { + self.palette.preview(ctx); + } + match &command.kind { - CommandKind::Focus(cmd) => match cmd { - FocusCommand::ModalClose => { - self.cancel(ctx); - } - FocusCommand::ListNext => { - self.next(ctx); - } - FocusCommand::ListNextPage => { - self.next_page(ctx); - } - FocusCommand::ListPrevious => { - self.previous(ctx); - } - FocusCommand::ListPreviousPage => { - self.previous_page(ctx); - } - FocusCommand::ListSelect => { - self.select(ctx); - } - _ => return CommandExecuted::No, - }, + CommandKind::Focus(FocusCommand::ModalClose) => { + self.cancel(ctx); + } CommandKind::Edit(cmd) => match cmd { EditCommand::DeleteBackward => { self.delete_backward(ctx); @@ -446,15 +417,21 @@ fn run_command( } impl PaletteData { - pub fn new(proxy: Arc) -> Self { + pub fn new(config: Arc, proxy: Arc) -> Self { let (sender, receiver) = unbounded(); let widget_id = WidgetId::next(); let scroll_id = WidgetId::next(); let preview_editor = WidgetId::next(); + let mut list_data = + ListData::new(config, widget_id, PaletteListData { workspace: None }); + // TODO: Make these configurable + list_data.line_height = Some(25); + list_data.max_displayed_items = 15; Self { widget_id, scroll_id, status: PaletteStatus::Inactive, + list_data, proxy, palette_type: PaletteType::File, sender, @@ -462,10 +439,8 @@ pub fn new(proxy: Arc) -> Self { run_id: Uuid::new_v4().to_string(), input: "".to_string(), cursor: 0, - index: 0, has_nonzero_default_index: false, - items: Vec::new(), - filtered_items: Vec::new(), + total_items: im::Vector::new(), preview_editor, input_editor: WidgetId::next(), } @@ -479,28 +454,20 @@ pub fn is_empty(&self) -> bool { self.len() == 0 } - pub fn current_items(&self) -> &Vec { + pub fn current_items(&self) -> &im::Vector { if self.get_input() == "" { - &self.items + &self.total_items } else { - &self.filtered_items + &self.list_data.items } } pub fn preview(&self, ctx: &mut EventCtx) { - if let Some(item) = self.get_item() { + if let Some(item) = self.list_data.current_selected_item() { item.content.select(ctx, true, self.preview_editor); } } - pub fn get_item(&self) -> Option<&PaletteItem> { - let items = self.current_items(); - if items.is_empty() { - return None; - } - Some(&items[self.index]) - } - pub fn get_input(&self) -> &str { match &self.palette_type { PaletteType::File => &self.input, @@ -518,19 +485,15 @@ pub fn get_input(&self) -> &str { } } -// TODO: Make this configurable -/// The maximum number of palette items to display per 'page' -pub const MAX_PALETTE_ITEMS: usize = 15; impl PaletteViewData { pub fn cancel(&mut self, ctx: &mut EventCtx) { let palette = Arc::make_mut(&mut self.palette); palette.status = PaletteStatus::Inactive; palette.input = "".to_string(); palette.cursor = 0; - palette.index = 0; palette.palette_type = PaletteType::File; - palette.items.clear(); - palette.filtered_items.clear(); + palette.total_items.clear(); + palette.list_data.clear_items(); if let Some(active) = *self.main_split.active_tab { ctx.submit_command(Command::new( LAPCE_UI_COMMAND, @@ -552,7 +515,7 @@ pub fn run_references( locations: &[EditorLocation], ) { self.run(ctx, Some(PaletteType::Reference), None); - let items: Vec = locations + let items = locations .iter() .map(|l| { let full_path = l.path.clone(); @@ -573,8 +536,9 @@ pub fn run_references( }) .collect(); let palette = Arc::make_mut(&mut self.palette); - palette.items = items; + palette.total_items = items; palette.preview(ctx); + self.fill_list(); } pub fn run( @@ -592,11 +556,10 @@ pub fn run( LapceUICommand::InitPaletteInput(palette.input.clone()), Target::Widget(*self.main_split.tab_id), )); - palette.items = Vec::new(); - palette.filtered_items = Vec::new(); + palette.total_items.clear(); + palette.list_data.clear_items(); palette.run_id = Uuid::new_v4().to_string(); palette.cursor = palette.input.len(); - palette.index = 0; if let Some(active_editor_content) = self.main_split.active_editor().map(|e| e.content.clone()) @@ -653,6 +616,16 @@ pub fn run( } } } + + self.fill_list(); + } + + /// Fill the list with the stored unfiltered total items + fn fill_list(&mut self) { + if self.palette.input.is_empty() { + Arc::make_mut(&mut self.palette).list_data.items = + self.palette.total_items.clone(); + } } fn delete_backward(&mut self, ctx: &mut EventCtx) { @@ -696,51 +669,16 @@ pub fn delete_to_beginning_of_line(&mut self, ctx: &mut EventCtx) { self.update_palette(ctx); } - pub fn next(&mut self, ctx: &mut EventCtx) { - let palette = Arc::make_mut(&mut self.palette); - palette.index = - Movement::Down.update_index(palette.index, palette.len(), 1, true); - palette.preview(ctx); - } - - pub fn next_page(&mut self, ctx: &mut EventCtx) { - let palette = Arc::make_mut(&mut self.palette); - palette.index = Movement::Down.update_index( - palette.index, - palette.len(), - MAX_PALETTE_ITEMS - 1, - false, - ); - palette.preview(ctx); - } - - pub fn previous(&mut self, ctx: &mut EventCtx) { - let palette = Arc::make_mut(&mut self.palette); - palette.index = - Movement::Up.update_index(palette.index, palette.len(), 1, true); - palette.preview(ctx); - } - - pub fn previous_page(&mut self, ctx: &mut EventCtx) { - let palette = Arc::make_mut(&mut self.palette); - palette.index = Movement::Up.update_index( - palette.index, - palette.len(), - MAX_PALETTE_ITEMS - 1, - false, - ); - palette.preview(ctx); - } - + // TODO: This is a bit weird, its wanting to iterate over items, but it could be called before we fill the list! fn preselect_matching(&mut self, ctx: &mut EventCtx, matching: &str) { let palette = Arc::make_mut(&mut self.palette); if let Some((id, _)) = palette - .items + .total_items .iter() .enumerate() .find(|(_, item)| item.filter_text == matching) { - palette.index = id; + palette.list_data.selected_index = id; palette.has_nonzero_default_index = true; palette.preview(ctx); } @@ -759,7 +697,7 @@ pub fn select(&mut self, ctx: &mut EventCtx) { )); } let palette = Arc::make_mut(&mut self.palette); - if let Some(item) = palette.get_item() { + if let Some(item) = palette.list_data.current_selected_item() { if item.content.select(ctx, false, palette.preview_editor) { self.cancel(ctx); } @@ -803,14 +741,13 @@ pub fn update_input(&mut self, ctx: &mut EventCtx, input: String) { // Update the current input palette.input = input; - self.update_palette(ctx) + self.update_palette(ctx); } pub fn update_palette(&mut self, ctx: &mut EventCtx) { let palette = Arc::make_mut(&mut self.palette); - if !palette.has_nonzero_default_index { - palette.index = 0; + palette.list_data.selected_index = 0; } palette.has_nonzero_default_index = false; @@ -823,14 +760,17 @@ pub fn update_palette(&mut self, ctx: &mut EventCtx) { return; } - if self.palette.get_input() != "" { + if self.palette.get_input() == "" { + self.palette.preview(ctx); + Arc::make_mut(&mut self.palette).list_data.items = + self.palette.total_items.clone(); + } else { + // Update the filtering with the input let _ = self.palette.sender.send(( self.palette.run_id.clone(), self.palette.get_input().to_string(), - self.palette.items.clone(), + self.palette.total_items.clone(), )); - } else { - self.palette.preview(ctx); } } @@ -841,7 +781,7 @@ fn get_files(&self, ctx: &mut EventCtx) { let event_sink = ctx.get_external_handle(); self.palette.proxy.proxy_rpc.get_files(move |result| { if let Ok(ProxyResponse::GetFilesResponse { items }) = result { - let items: Vec = items + let items: im::Vector = items .iter() .enumerate() .map(|(_index, path)| { @@ -882,7 +822,7 @@ fn get_ssh_hosts(&mut self, _ctx: &mut EventCtx) { } let palette = Arc::make_mut(&mut self.palette); - palette.items = hosts + palette.total_items = hosts .iter() .map(|(user, host)| PaletteItem { content: PaletteItemContent::SshHost( @@ -899,7 +839,7 @@ fn get_ssh_hosts(&mut self, _ctx: &mut EventCtx) { fn get_workspaces(&mut self, _ctx: &mut EventCtx) { let workspaces = Config::recent_workspaces().unwrap_or_default(); let palette = Arc::make_mut(&mut self.palette); - palette.items = workspaces + palette.total_items = workspaces .into_iter() .map(|w| { let text = w @@ -930,7 +870,7 @@ fn get_workspaces(&mut self, _ctx: &mut EventCtx) { fn get_themes(&mut self, _ctx: &mut EventCtx, config: &Config) { let palette = Arc::make_mut(&mut self.palette); - palette.items = config + palette.total_items = config .available_themes .values() .sorted_by_key(|(n, _)| n) @@ -947,7 +887,7 @@ fn get_languages(&mut self, _ctx: &mut EventCtx) { let palette = Arc::make_mut(&mut self.palette); let mut langs = LapceLanguage::languages(); langs.push("Plain Text".to_string()); - palette.items = langs + palette.total_items = langs .iter() .sorted() .map(|n| PaletteItem { @@ -963,7 +903,7 @@ fn get_commands(&mut self, _ctx: &mut EventCtx) { const EXCLUDED_ITEMS: &[&str] = &["palette.command"]; let palette = Arc::make_mut(&mut self.palette); - palette.items = self + palette.total_items = self .keypress .commands .iter() @@ -989,7 +929,7 @@ fn get_lines(&mut self, _ctx: &mut EventCtx) { { let raw = terminal.raw.lock(); let term = &raw.term; - let mut items = Vec::new(); + let mut items = im::Vector::new(); let mut last_row: Option = None; let mut current_line = term.topmost_line().0; for line in term.topmost_line().0..term.bottommost_line().0 { @@ -1020,11 +960,11 @@ fn get_lines(&mut self, _ctx: &mut EventCtx) { score: 0, indices: vec![], }; - items.push(item); + items.push_back(item); } } let palette = Arc::make_mut(&mut self.palette); - palette.items = items; + palette.total_items = items; } return; } @@ -1038,7 +978,7 @@ fn get_lines(&mut self, _ctx: &mut EventCtx) { let last_line_number = doc.buffer().last_line() + 1; let last_line_number_len = last_line_number.to_string().len(); let palette = Arc::make_mut(&mut self.palette); - palette.items = doc + palette.total_items = doc .buffer() .text() .lines(0..doc.buffer().len()) @@ -1083,7 +1023,7 @@ fn get_document_symbols(&mut self, ctx: &mut EventCtx) { .proxy_rpc .get_document_symbols(path, move |result| { if let Ok(ProxyResponse::GetDocumentSymbols { resp }) = result { - let items: Vec = match resp { + let items: im::Vector = match resp { DocumentSymbolResponse::Flat(symbols) => symbols .iter() .map(|s| { @@ -1156,7 +1096,7 @@ fn get_workspace_symbols(&mut self, ctx: &mut EventCtx) { if let Ok(ProxyResponse::GetWorkspaceSymbols { symbols }) = result { - let items: Vec = symbols + let items: im::Vector = symbols .iter() .map(|s| { // TODO: Should we be using filter text? @@ -1196,13 +1136,13 @@ fn get_workspace_symbols(&mut self, ctx: &mut EventCtx) { } pub fn update_process( - receiver: Receiver<(String, String, Vec)>, + receiver: Receiver<(String, String, im::Vector)>, widget_id: WidgetId, event_sink: ExtEventSink, ) { fn receive_batch( - receiver: &Receiver<(String, String, Vec)>, - ) -> Result<(String, String, Vec)> { + receiver: &Receiver<(String, String, im::Vector)>, + ) -> Result<(String, String, im::Vector)> { let (mut run_id, mut input, mut items) = receiver.recv()?; loop { match receiver.try_recv() { @@ -1242,10 +1182,10 @@ fn receive_batch( fn filter_items( _run_id: &str, input: &str, - items: Vec, + items: im::Vector, matcher: &SkimMatcherV2, - ) -> Vec { - let mut items: Vec = items + ) -> im::Vector { + let mut items: im::Vector = items .iter() .filter_map(|i| { if let Some((score, indices)) = diff --git a/lapce-ui/src/completion.rs b/lapce-ui/src/completion.rs index 45da376f..8aeb33ae 100644 --- a/lapce-ui/src/completion.rs +++ b/lapce-ui/src/completion.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::{fmt::Display, sync::Arc}; use anyhow::Error; use druid::{ @@ -14,6 +14,7 @@ completion::{CompletionData, CompletionStatus, ScoredCompletionItem}, config::{Config, LapceTheme}, data::LapceTabData, + list::ListData, markdown::parse_markdown, rich_text::{RichText, RichTextBuilder}, }; @@ -22,6 +23,7 @@ use std::str::FromStr; use crate::{ + list::{List, ListPaint}, scroll::{LapceIdentityWrapper, LapceScroll}, svg::completion_svg, }; @@ -182,8 +184,8 @@ pub struct CompletionContainer { id: WidgetId, scroll_id: WidgetId, completion: WidgetPod< - LapceTabData, - LapceIdentityWrapper>, + ListData, + List, >, completion_content_size: Size, documentation: WidgetPod< @@ -195,10 +197,7 @@ pub struct CompletionContainer { impl CompletionContainer { pub fn new(data: &CompletionData) -> Self { - let completion = LapceIdentityWrapper::wrap( - LapceScroll::new(Completion::new()).vertical(), - data.scroll_id, - ); + let completion = List::new(data.scroll_id); let completion_doc = LapceIdentityWrapper::wrap( LapceScroll::new(CompletionDocumentation::new()).vertical(), data.documentation_scroll_id, @@ -220,19 +219,15 @@ pub fn ensure_item_visible( env: &Env, ) { let width = ctx.size().width; - let line_height = data.config.editor.line_height() as f64; + let line_height = data.completion.completion_list.line_height() as f64; + let rect = Size::new(width, line_height) .to_rect() .with_origin(Point::new( 0.0, - data.completion.index as f64 * line_height, + data.completion.completion_list.selected_index as f64 * line_height, )); - if self - .completion - .widget_mut() - .inner_mut() - .scroll_to_visible(rect, env) - { + if self.completion.widget_mut().scroll_to_visible(rect, env) { ctx.submit_command(Command::new( LAPCE_UI_COMMAND, LapceUICommand::ResetFade, @@ -248,9 +243,12 @@ pub fn ensure_item_top_visible( ctx: &mut EventCtx, data: &LapceTabData, ) { - let line_height = data.config.editor.line_height() as f64; - let point = Point::new(0.0, data.completion.index as f64 * line_height); - if self.completion.widget_mut().inner_mut().scroll_to(point) { + let line_height = data.completion.completion_list.line_height() as f64; + let point = Point::new( + 0.0, + data.completion.completion_list.selected_index as f64 * line_height, + ); + if self.completion.widget_mut().scroll_to(point) { ctx.submit_command(Command::new( LAPCE_UI_COMMAND, LapceUICommand::ResetFade, @@ -266,7 +264,8 @@ fn update_documentation(&mut self, data: &LapceTabData) { let documentation = if data.config.editor.completion_show_documentation { let current_item = (!data.completion.is_empty()) - .then(|| data.completion.current_item()); + .then(|| data.completion.current_item()) + .flatten(); current_item.and_then(|item| item.item.documentation.as_ref()) } else { @@ -308,7 +307,35 @@ fn event( data: &mut LapceTabData, env: &Env, ) { - self.completion.event(ctx, event, data, env); + match event { + Event::Command(cmd) if cmd.is(LAPCE_UI_COMMAND) => { + let command = cmd.get_unchecked(LAPCE_UI_COMMAND); + if let LapceUICommand::ListItemSelected = command { + if let Some(editor) = data + .main_split + .active + .and_then(|active| data.main_split.editors.get(&active)) + .cloned() + { + let mut editor_data = + data.editor_view_content(editor.view_id); + let doc = editor_data.doc.clone(); + editor_data.completion_item_select(ctx); + data.update_from_editor_buffer_data( + editor_data, + &editor, + &doc, + ); + } + } + } + _ => {} + } + + let completion = Arc::make_mut(&mut data.completion); + completion.completion_list.update_data(data.config.clone()); + self.completion + .event(ctx, event, &mut completion.completion_list, env); self.documentation.event(ctx, event, data, env); } @@ -319,7 +346,15 @@ fn lifecycle( data: &LapceTabData, env: &Env, ) { - self.completion.lifecycle(ctx, event, data, env); + self.completion.lifecycle( + ctx, + event, + &data + .completion + .completion_list + .clone_with(data.config.clone()), + env, + ); self.documentation.lifecycle(ctx, event, data, env); } @@ -353,14 +388,8 @@ fn update( if old_data.completion.input != data.completion.input || old_data.completion.request_id != data.completion.request_id || old_data.completion.status != data.completion.status - || !old_data - .completion - .current_items() - .same(data.completion.current_items()) - || !old_data - .completion - .filtered_items - .same(&data.completion.filtered_items) + || old_data.completion.completion_list.items + != data.completion.completion_list.items { self.update_documentation(data); ctx.request_layout(); @@ -377,7 +406,9 @@ fn update( )); } - if old_completion.index != completion.index { + if old_completion.completion_list.selected_index + != completion.completion_list.selected_index + { self.ensure_item_visible(ctx, data, env); self.update_documentation(data); ctx.request_paint(); @@ -393,20 +424,38 @@ fn update( { ctx.request_layout(); } + + self.completion.update( + ctx, + &data + .completion + .completion_list + .clone_with(data.config.clone()), + env, + ); } } fn layout( &mut self, ctx: &mut LayoutCtx, - _bc: &BoxConstraints, + bc: &BoxConstraints, data: &LapceTabData, env: &Env, ) -> Size { - let completion_size = data.completion.size; - let bc = BoxConstraints::new(Size::ZERO, completion_size); - self.completion_content_size = self.completion.layout(ctx, &bc, data, env); - self.completion.set_origin(ctx, data, env, Point::ZERO); + // TODO: Let this be configurable + let width = 400.0; + + let bc = BoxConstraints::tight(Size::new(width, bc.max().height)); + + let completion_list = data + .completion + .completion_list + .clone_with(data.config.clone()); + self.completion_content_size = + self.completion.layout(ctx, &bc, &completion_list, env); + self.completion + .set_origin(ctx, &completion_list, env, Point::ZERO); // Position the documentation over the current completion item to the right let documentation_size = data.completion.documentation_size; @@ -423,8 +472,10 @@ fn layout( ctx.set_paint_insets((10.0, 10.0, 10.0, 10.0)); Size::new( - completion_size.width + documentation_size.width, - completion_size.height.max(documentation_size.height), + self.completion_content_size.width + documentation_size.width, + self.completion_content_size + .height + .max(documentation_size.height), ) } @@ -433,6 +484,15 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { && data.completion.len() > 0 { let rect = self.completion_content_size.to_rect(); + + // Draw the background + ctx.fill( + rect, + data.config + .get_color_unchecked(LapceTheme::COMPLETION_BACKGROUND), + ); + + // Draw the shadow let shadow_width = data.config.ui.drop_shadow_width() as f64; if shadow_width > 0.0 { ctx.blurred_rect( @@ -448,160 +508,91 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { 1.0, ); } - self.completion.paint(ctx, data, env); + + // Draw the completion list over the background + self.completion.paint( + ctx, + &data + .completion + .completion_list + .clone_with(data.config.clone()), + env, + ); + + // Draw the documentation to the side self.documentation.paint(ctx, data, env); } } } -pub struct Completion {} - -impl Completion { - pub fn new() -> Self { - Self {} - } -} - -impl Default for Completion { - fn default() -> Self { - Self::new() - } -} - -impl Widget for Completion { - fn event( - &mut self, - _ctx: &mut EventCtx, - _event: &Event, - _data: &mut LapceTabData, +impl ListPaint for ScoredCompletionItem { + fn paint( + &self, + ctx: &mut PaintCtx, + data: &ListData, _env: &Env, + line: usize, ) { - } - - fn lifecycle( - &mut self, - _ctx: &mut LifeCycleCtx, - _event: &LifeCycle, - _data: &LapceTabData, - _env: &Env, - ) { - } - - fn update( - &mut self, - _ctx: &mut UpdateCtx, - _old_data: &LapceTabData, - _data: &LapceTabData, - _env: &Env, - ) { - } - - fn layout( - &mut self, - _ctx: &mut LayoutCtx, - bc: &BoxConstraints, - data: &LapceTabData, - _env: &Env, - ) -> Size { - let line_height = data.config.editor.line_height() as f64; - let height = data.completion.len(); - let height = height as f64 * line_height; - Size::new(bc.max().width, height) - } - - fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, _env: &Env) { - if data.completion.status == CompletionStatus::Inactive { - return; - } - let line_height = data.config.editor.line_height() as f64; - let rect = ctx.region().bounding_box(); let size = ctx.size(); - - let _input = &data.completion.input; - let items: &Vec = data.completion.current_items(); - - ctx.fill( - rect, - data.config - .get_color_unchecked(LapceTheme::COMPLETION_BACKGROUND), - ); - - let start_line = (rect.y0 / line_height).floor() as usize; - let end_line = (rect.y1 / line_height).ceil() as usize; - - for line in start_line..end_line { - if line >= items.len() { - break; - } - - if line == data.completion.index { - ctx.fill( - Rect::ZERO - .with_origin(Point::new(0.0, line as f64 * line_height)) - .with_size(Size::new(size.width, line_height)), - data.config - .get_color_unchecked(LapceTheme::COMPLETION_CURRENT), - ); - } - - let item = &items[line]; - - if let Some((svg, color)) = completion_svg(item.item.kind, &data.config) - { - let color = color.unwrap_or_else(|| { - data.config - .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) - .clone() - }); - let rect = Size::new(line_height, line_height) - .to_rect() - .with_origin(Point::new(0.0, line_height * line as f64)); - ctx.fill(rect, &color.clone().with_alpha(0.3)); - - let width = 16.0; - let height = 16.0; - let rect = - Size::new(width, height).to_rect().with_origin(Point::new( - (line_height - width) / 2.0, - (line_height - height) / 2.0 + line_height * line as f64, - )); - ctx.draw_svg(&svg, rect, Some(&color)); - } - - let focus_color = - data.config.get_color_unchecked(LapceTheme::EDITOR_FOCUS); - let content = item.item.label.as_str(); - - let mut text_layout = ctx - .text() - .new_text_layout(content.to_string()) - .font( - FontFamily::new_unchecked( - data.config.editor.font_family.clone(), - ), - data.config.editor.font_size as f64, - ) - .text_color( - data.config - .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) - .clone(), - ); - for i in &item.indices { - let i = *i; - text_layout = text_layout.range_attribute( - i..i + 1, - TextAttribute::TextColor(focus_color.clone()), - ); - text_layout = text_layout.range_attribute( - i..i + 1, - TextAttribute::Weight(FontWeight::BOLD), - ); - } - let text_layout = text_layout.build().unwrap(); - let y = line_height * line as f64 + text_layout.y_offset(line_height); - let point = Point::new(line_height + 5.0, y); - ctx.draw_text(&text_layout, point); + let line_height = data.line_height() as f64; + if line == data.selected_index { + ctx.fill( + Rect::ZERO + .with_origin(Point::new(0.0, line as f64 * line_height)) + .with_size(Size::new(size.width, line_height)), + data.config + .get_color_unchecked(LapceTheme::COMPLETION_CURRENT), + ); } + + if let Some((svg, color)) = completion_svg(self.item.kind, &data.config) { + let color = color.unwrap_or_else(|| { + data.config + .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) + .clone() + }); + let rect = Size::new(line_height, line_height) + .to_rect() + .with_origin(Point::new(0.0, line_height * line as f64)); + ctx.fill(rect, &color.clone().with_alpha(0.3)); + + let width = 16.0; + let height = 16.0; + let rect = Size::new(width, height).to_rect().with_origin(Point::new( + (line_height - width) / 2.0, + (line_height - height) / 2.0 + line_height * line as f64, + )); + ctx.draw_svg(&svg, rect, Some(&color)); + } + + let focus_color = data.config.get_color_unchecked(LapceTheme::EDITOR_FOCUS); + let content = self.item.label.as_str(); + + let mut text_layout = ctx + .text() + .new_text_layout(content.to_string()) + .font( + FontFamily::new_unchecked(data.config.editor.font_family.clone()), + data.config.editor.font_size as f64, + ) + .text_color( + data.config + .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) + .clone(), + ); + for i in &self.indices { + let i = *i; + text_layout = text_layout.range_attribute( + i..i + 1, + TextAttribute::TextColor(focus_color.clone()), + ); + text_layout = text_layout + .range_attribute(i..i + 1, TextAttribute::Weight(FontWeight::BOLD)); + } + let text_layout = text_layout.build().unwrap(); + let y = line_height * line as f64 + text_layout.y_offset(line_height); + let point = Point::new(line_height + 5.0, y); + ctx.draw_text(&text_layout, point); } } diff --git a/lapce-ui/src/lib.rs b/lapce-ui/src/lib.rs index f9fa0211..37eba77b 100644 --- a/lapce-ui/src/lib.rs +++ b/lapce-ui/src/lib.rs @@ -8,6 +8,7 @@ pub mod find; pub mod hover; pub mod keymap; +pub mod list; mod logging; pub mod palette; pub mod panel; diff --git a/lapce-ui/src/list.rs b/lapce-ui/src/list.rs new file mode 100644 index 00000000..8bbd049e --- /dev/null +++ b/lapce-ui/src/list.rs @@ -0,0 +1,292 @@ +use std::marker::PhantomData; + +use druid::{ + BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, + PaintCtx, Point, Rect, RenderContext, Size, UpdateCtx, Widget, WidgetId, + WidgetPod, +}; +use lapce_data::{config::LapceTheme, list::ListData}; + +use crate::scroll::{LapceIdentityWrapper, LapceScroll}; + +pub struct DropdownButton + PartialEq + 'static, D: Data> { + pub list: WidgetPod, List>, +} + +// TODO: Support optional multi-select + +/// Contains a list of choices of type `T` +/// The type must be cloneable, list-paintable, and able to be compared. +/// `D` is associated data that users of `List` may need, since the painting is only given +/// `&ListData` for use. +/// +/// We let the `KeyPressFocus` be handled by the containing widget, since widgets like palette +/// want focus for themselves. You can call `ListData::run_command` to use its sensible +/// defaults for movement and the like. +pub struct List + PartialEq + 'static, D: Data> { + content_rect: Rect, + // I don't see a way to break this apart that doesn't make it less clear + #[allow(clippy::type_complexity)] + content: WidgetPod< + ListData, + LapceIdentityWrapper, ListContent>>, + >, +} +impl + PartialEq + 'static, D: Data> List { + pub fn new(scroll_id: WidgetId) -> List { + let content = LapceIdentityWrapper::wrap( + LapceScroll::new(ListContent::new()).vertical(), + scroll_id, + ); + List { + content_rect: Rect::ZERO, + content: WidgetPod::new(content), + } + } + + pub fn scroll_to(&mut self, point: Point) -> bool { + self.content.widget_mut().inner_mut().scroll_to(point) + } + + pub fn scroll_to_visible(&mut self, rect: Rect, env: &Env) -> bool { + self.content + .widget_mut() + .inner_mut() + .scroll_to_visible(rect, env) + } +} +impl + PartialEq + 'static, D: Data> Widget> + for List +{ + fn event( + &mut self, + ctx: &mut EventCtx, + event: &Event, + data: &mut ListData, + env: &Env, + ) { + self.content.event(ctx, event, data, env); + } + + fn lifecycle( + &mut self, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &ListData, + env: &Env, + ) { + self.content.lifecycle(ctx, event, data, env); + } + + fn update( + &mut self, + ctx: &mut UpdateCtx, + _old_data: &ListData, + data: &ListData, + env: &Env, + ) { + self.content.update(ctx, data, env); + } + + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &ListData, + env: &Env, + ) -> Size { + // TODO: Allow restricting the max height? Technically that can just be done by restricting the + // maximum number of displayed items + + // The width are given by whatever widget contains the list + let width = bc.max().width; + + let line_height = data.line_height() as f64; + + let count = data.max_display_count(); + // The height of the rendered entries + let height = count as f64 * line_height; + + // TODO: Have an option to let us fill the rest of the space with empty background + // Since some lists may want to take up all the space they're given + + // Create a bc which only contains the list we're actually rendering + let bc = BoxConstraints::tight(Size::new(width, height)); + + let content_size = self.content.layout(ctx, &bc, data, env); + self.content.set_origin(ctx, data, env, Point::ZERO); + let mut content_height = content_size.height; + // Add padding to the bottom + if content_height > 0.0 { + content_height += 5.0; + } + + let self_size = Size::new(content_size.width, content_height); + + self.content_rect = self_size.to_rect().with_origin(Point::ZERO); + + self_size + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &ListData, env: &Env) { + let rect = self.content_rect; + // TODO: Put whether shadows should appear on the list behind a variable + let shadow_width = data.config.ui.drop_shadow_width() as f64; + if shadow_width > 0.0 { + ctx.blurred_rect( + rect, + shadow_width, + data.config + .get_color_unchecked(LapceTheme::LAPCE_DROPDOWN_SHADOW), + ); + } else { + ctx.stroke( + rect.inflate(0.5, 0.5), + data.config.get_color_unchecked(LapceTheme::LAPCE_BORDER), + 1.0, + ); + } + // TODO: We could have the user of this provide a custom color + // Or we could say that they have to fill it? Since something like + // palette also wants to draw their background behind the other bits + // and so, if we painted here, we'd double-paint + // which would be annoying for transparent colors. + // ctx.fill( + // rect, + // data.config + // .get_color_unchecked(LapceTheme::PALETTE_BACKGROUND), + // ); + + self.content.paint(ctx, data, env); + } +} + +/// The actual list of entries +struct ListContent + 'static, D: Data> { + /// The line the mouse was last down upon + mouse_down: usize, + _marker: PhantomData<(*const T, *const D)>, +} +impl + 'static, D: Data> ListContent { + pub fn new() -> ListContent { + ListContent { + mouse_down: 0, + _marker: PhantomData, + } + } +} +impl + 'static, D: Data> Widget> + for ListContent +{ + fn event( + &mut self, + ctx: &mut EventCtx, + event: &Event, + data: &mut ListData, + _env: &Env, + ) { + match event { + Event::MouseMove(_) => { + ctx.set_cursor(&druid::Cursor::Pointer); + ctx.set_handled(); + } + Event::MouseDown(mouse_event) => { + let line = + (mouse_event.pos.y / data.line_height() as f64).floor() as usize; + self.mouse_down = line; + ctx.set_handled(); + } + Event::MouseUp(mouse_event) => { + // TODO: function for translating mouse pos to the line, so that we don't repeat + // this calculation; which makes it harder to change later + let line = + (mouse_event.pos.y / data.line_height() as f64).floor() as usize; + if line == self.mouse_down { + data.selected_index = line; + data.select(ctx); + } + ctx.set_handled(); + } + _ => {} + } + } + + fn lifecycle( + &mut self, + _ctx: &mut LifeCycleCtx, + _event: &LifeCycle, + _data: &ListData, + _env: &Env, + ) { + } + + fn update( + &mut self, + _ctx: &mut UpdateCtx, + _old_data: &ListData, + _data: &ListData, + _env: &Env, + ) { + } + + fn layout( + &mut self, + _ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &ListData, + _env: &Env, + ) -> Size { + let line_height = data.line_height() as f64; + // We include the total number of items because we should be in a scroll widget + // and that needs the overall height, not just the rendered height. + let height = line_height * data.items.len() as f64; + + Size::new(bc.max().width, height) + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &ListData, env: &Env) { + let rect = ctx.region().bounding_box(); + let size = ctx.size(); + + let line_height = data.line_height() as f64; + + let start_line = (rect.y0 / line_height).floor() as usize; + let end_line = (rect.y1 / line_height).ceil() as usize; + let count = end_line - start_line; + + // Get the items, skip over all items before the start line, and + // ignore all items after the end line + for (line, item) in + data.items.iter().enumerate().skip(start_line).take(count) + { + if line == data.selected_index { + // Create a rect covering the entry at the selected index + let bg_rect = Rect::ZERO + .with_origin(Point::new(0.0, line as f64 * line_height)) + .with_size(Size::new(size.width, line_height)); + + // TODO: Give this its own theme name entry + ctx.fill( + bg_rect, + data.config.get_color_unchecked(LapceTheme::PALETTE_CURRENT), + ); + } + + item.paint(ctx, data, env, line); + } + } +} + +/// A trait for painting relatively simple elements that are put in a list +/// They don't get a say in their layout or custom handling of events +/// +/// Takes an immutable reference, due to `data` containing this entry +pub trait ListPaint: Sized + Clone { + fn paint( + &self, + ctx: &mut PaintCtx, + data: &ListData, + env: &Env, + line: usize, + ); +} diff --git a/lapce-ui/src/palette.rs b/lapce-ui/src/palette.rs index bdfd1dcc..7d90271f 100644 --- a/lapce-ui/src/palette.rs +++ b/lapce-ui/src/palette.rs @@ -12,21 +12,21 @@ }; use druid::{FontWeight, Modifiers}; use lapce_data::command::LAPCE_COMMAND; -use lapce_data::config::Config; use lapce_data::data::LapceWorkspaceType; -use lapce_data::palette::{PaletteItemContent, MAX_PALETTE_ITEMS}; +use lapce_data::list::ListData; +use lapce_data::palette::{PaletteItem, PaletteItemContent, PaletteListData}; use lapce_data::{ command::{LapceUICommand, LAPCE_UI_COMMAND}, config::LapceTheme, data::LapceTabData, keypress::KeyPressFocus, - palette::{PaletteStatus, PaletteType, PaletteViewData, PaletteViewLens}, + palette::{PaletteStatus, PaletteType, PaletteViewData}, }; use lsp_types::SymbolKind; +use crate::list::{List, ListPaint}; use crate::{ editor::view::LapceEditorView, - scroll::{LapceIdentityWrapper, LapceScroll}, svg::{file_svg, symbol_svg}, }; @@ -135,13 +135,16 @@ fn event( LapceUICommand::UpdatePaletteItems(run_id, items) => { let palette = Arc::make_mut(&mut data.palette); if &palette.run_id == run_id { - palette.items = items.to_owned(); + palette.total_items = items.clone(); palette.preview(ctx); - if palette.get_input() != "" { + if palette.get_input() == "" { + palette.list_data.items = + palette.total_items.clone(); + } else { let _ = palette.sender.send(( palette.run_id.clone(), palette.get_input().to_string(), - palette.items.clone(), + palette.total_items.clone(), )); } } @@ -154,10 +157,13 @@ fn event( let palette = Arc::make_mut(&mut data.palette); if &palette.run_id == run_id && palette.get_input() == input { - palette.filtered_items = filtered_items.to_owned(); + palette.list_data.items = filtered_items.clone(); palette.preview(ctx); } } + LapceUICommand::ListItemSelected => { + data.palette_view_data().select(ctx); + } _ => {} } } @@ -215,14 +221,11 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { struct PaletteContainer { content_rect: Rect, - line_height: f64, input: WidgetPod>>, #[allow(clippy::type_complexity)] content: WidgetPod< - LapceTabData, - LapceIdentityWrapper< - LapceScroll>>, - >, + ListData, + List, >, preview: WidgetPod>>, } @@ -239,11 +242,7 @@ pub fn new(data: &LapceTabData) -> Self { .hide_header() .hide_gutter() .padding((10.0, 5.0, 10.0, 5.0)); - let content = LapceIdentityWrapper::wrap( - LapceScroll::new(PaletteContent::new().lens(PaletteViewLens).boxed()) - .vertical(), - data.palette.scroll_id, - ); + let content = List::new(data.palette.scroll_id); let preview = LapceEditorView::new(preview_editor.view_id, WidgetId::next(), None); Self { @@ -251,7 +250,6 @@ pub fn new(data: &LapceTabData) -> Self { input: WidgetPod::new(input.boxed()), content: WidgetPod::new(content), preview: WidgetPod::new(preview.boxed()), - line_height: 25.0, } } @@ -262,19 +260,17 @@ fn ensure_item_visible( env: &Env, ) { let width = ctx.size().width; + // TODO: This function could just be on List? + let line_height = data.palette.list_data.line_height() as f64; let rect = - Size::new(width, self.line_height) + Size::new(width, line_height as f64) .to_rect() .with_origin(Point::new( 0.0, - data.palette.index as f64 * self.line_height, + data.palette.list_data.selected_index as f64 + * line_height as f64, )); - if self - .content - .widget_mut() - .inner_mut() - .scroll_to_visible(rect, env) - { + if self.content.widget_mut().scroll_to_visible(rect, env) { ctx.submit_command(Command::new( LAPCE_UI_COMMAND, LapceUICommand::ResetFade, @@ -293,7 +289,11 @@ fn event( env: &Env, ) { self.input.event(ctx, event, data, env); - self.content.event(ctx, event, data, env); + + let palette = Arc::make_mut(&mut data.palette); + palette.list_data.update_data(data.config.clone()); + self.content.event(ctx, event, &mut palette.list_data, env); + self.preview.event(ctx, event, data, env); } @@ -305,7 +305,12 @@ fn lifecycle( env: &Env, ) { self.input.lifecycle(ctx, event, data, env); - self.content.lifecycle(ctx, event, data, env); + self.content.lifecycle( + ctx, + event, + &data.palette.list_data.clone_with(data.config.clone()), + env, + ); self.preview.lifecycle(ctx, event, data, env); } @@ -317,13 +322,18 @@ fn update( env: &Env, ) { if old_data.palette.input != data.palette.input - || old_data.palette.index != data.palette.index + || old_data.palette.list_data.selected_index + != data.palette.list_data.selected_index || old_data.palette.run_id != data.palette.run_id { self.ensure_item_visible(ctx, data, env); } self.input.update(ctx, data, env); - self.content.update(ctx, data, env); + self.content.update( + ctx, + &data.palette.list_data.clone_with(data.config.clone()), + env, + ); self.preview.update(ctx, data, env); } @@ -337,28 +347,29 @@ fn layout( let width = bc.max().width; let max_height = bc.max().height; - let bc = BoxConstraints::tight(Size::new(width, bc.max().height)); + let bc = BoxConstraints::tight(Size::new(width, max_height)); let input_size = self.input.layout(ctx, &bc, data, env); self.input.set_origin(ctx, data, env, Point::ZERO); - let height = MAX_PALETTE_ITEMS.min(data.palette.len()); - let height = self.line_height * height as f64; + let height = max_height - input_size.height; let bc = BoxConstraints::tight(Size::new(width, height)); - let content_size = self.content.layout(ctx, &bc, data, env); - self.content - .set_origin(ctx, data, env, Point::new(0.0, input_size.height)); - let mut content_height = content_size.height; - if content_height > 0.0 { - content_height += 5.0; - } + let content_size = + self.content.layout(ctx, &bc, &data.palette.list_data, env); + self.content.set_origin( + ctx, + &data.palette.list_data.clone_with(data.config.clone()), + env, + Point::new(0.0, input_size.height), + ); let max_preview_height = max_height - input_size.height - - MAX_PALETTE_ITEMS as f64 * self.line_height + - data.palette.list_data.max_displayed_items as f64 + * data.palette.list_data.line_height() as f64 - 5.0; let preview_height = if data.palette.palette_type.has_preview() { - if content_height > 0.0 { + if content_size.height > 0.0 { max_preview_height } else { 0.0 @@ -380,13 +391,15 @@ fn layout( ctx, data, env, - Point::new(preview_width / 2.0, input_size.height + content_height), + Point::new(preview_width / 2.0, input_size.height + content_size.height), ); ctx.set_paint_insets(4000.0); - let self_size = - Size::new(width, input_size.height + content_height + preview_height); + let self_size = Size::new( + width, + input_size.height + content_size.height + preview_height, + ); self.content_rect = Size::new(width, self_size.height) .to_rect() .with_origin(Point::new(0.0, 0.0)); @@ -424,11 +437,13 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { return; } - self.content.paint(ctx, data, env); + self.content.paint( + ctx, + &data.palette.list_data.clone_with(data.config.clone()), + env, + ); - if !data.palette.current_items().is_empty() - && data.palette.palette_type.has_preview() - { + if !data.palette.is_empty() && data.palette.palette_type.has_preview() { let rect = self.preview.layout_rect(); ctx.fill( rect, @@ -544,420 +559,6 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &PaletteViewData, _env: &Env) { } } -pub struct PaletteContent { - mouse_down: usize, - line_height: f64, -} - -impl PaletteContent { - pub fn new() -> Self { - Self { - mouse_down: 0, - line_height: 25.0, - } - } - - fn paint_palette_item( - palette_item_content: &PaletteItemContent, - ctx: &mut PaintCtx, - workspace_path: Option<&Path>, - line: usize, - indices: &[usize], - line_height: f64, - config: &Config, - ) { - let (svg, text, text_indices, hint, hint_indices) = - match palette_item_content { - PaletteItemContent::File(path, _) => { - Self::file_paint_items(path, indices) - } - PaletteItemContent::DocumentSymbol { - kind, - name, - container_name, - .. - } => { - let text = name.to_string(); - let hint = - container_name.clone().unwrap_or_else(|| "".to_string()); - let text_indices = indices - .iter() - .filter_map(|i| { - let i = *i; - if i < text.len() { - Some(i) - } else { - None - } - }) - .collect(); - let hint_indices = indices - .iter() - .filter_map(|i| { - let i = *i; - if i >= text.len() { - Some(i - text.len()) - } else { - None - } - }) - .collect(); - (symbol_svg(kind), text, text_indices, hint, hint_indices) - } - PaletteItemContent::WorkspaceSymbol { - kind, - name, - location, - .. - } => Self::file_paint_symbols( - &location.path, - indices, - workspace_path, - name.as_str(), - *kind, - ), - PaletteItemContent::Line(_, text) => { - (None, text.clone(), indices.to_vec(), "".to_string(), vec![]) - } - PaletteItemContent::ReferenceLocation(rel_path, _location) => { - Self::file_paint_items(rel_path, indices) - } - PaletteItemContent::Workspace(w) => { - let text = w.path.as_ref().unwrap().to_str().unwrap(); - let text = match &w.kind { - LapceWorkspaceType::Local => text.to_string(), - LapceWorkspaceType::RemoteSSH(user, host) => { - format!("[{user}@{host}] {text}") - } - LapceWorkspaceType::RemoteWSL => { - format!("[wsl] {text}") - } - }; - (None, text, indices.to_vec(), "".to_string(), vec![]) - } - PaletteItemContent::Command(command) => ( - None, - command - .kind - .desc() - .map(|m| m.to_string()) - .unwrap_or_else(|| "".to_string()), - indices.to_vec(), - "".to_string(), - vec![], - ), - PaletteItemContent::Theme(theme) => ( - None, - theme.to_string(), - indices.to_vec(), - "".to_string(), - vec![], - ), - PaletteItemContent::Language(name) => ( - None, - name.to_string(), - indices.to_vec(), - "".to_string(), - vec![], - ), - PaletteItemContent::TerminalLine(_line, content) => ( - None, - content.clone(), - indices.to_vec(), - "".to_string(), - vec![], - ), - PaletteItemContent::SshHost(user, host) => ( - None, - format!("{user}@{host}"), - indices.to_vec(), - "".to_string(), - vec![], - ), - }; - - if let Some(svg) = svg.as_ref() { - let width = 14.0; - let height = 14.0; - let rect = Size::new(width, height).to_rect().with_origin(Point::new( - (line_height - width) / 2.0 + 5.0, - (line_height - height) / 2.0 + line_height * line as f64, - )); - ctx.draw_svg(svg, rect, None); - } - - let svg_x = match palette_item_content { - &PaletteItemContent::Line(_, _) | &PaletteItemContent::Workspace(_) => { - 0.0 - } - _ => line_height, - }; - - let focus_color = config.get_color_unchecked(LapceTheme::EDITOR_FOCUS); - - let full_text = if hint.is_empty() { - text.clone() - } else { - text.clone() + " " + &hint - }; - let mut text_layout = ctx - .text() - .new_text_layout(full_text.clone()) - .font(config.ui.font_family(), config.ui.font_size() as f64) - .text_color( - config - .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) - .clone(), - ); - for &i_start in &text_indices { - let i_end = full_text - .char_indices() - .find(|(i, _)| *i == i_start) - .map(|(_, c)| c.len_utf8() + i_start); - let i_end = if let Some(i_end) = i_end { - i_end - } else { - // Log a warning, but continue as we don't want to crash on a bug - log::warn!( - "Invalid text indices in palette: text: '{}', i_start: {}", - text, - i_start - ); - continue; - }; - - text_layout = text_layout.range_attribute( - i_start..i_end, - TextAttribute::TextColor(focus_color.clone()), - ); - text_layout = text_layout.range_attribute( - i_start..i_end, - TextAttribute::Weight(FontWeight::BOLD), - ); - } - - if !hint.is_empty() { - text_layout = text_layout - .range_attribute( - text.len() + 1..full_text.len(), - TextAttribute::FontSize(13.0), - ) - .range_attribute( - text.len() + 1..full_text.len(), - TextAttribute::TextColor( - config.get_color_unchecked(LapceTheme::EDITOR_DIM).clone(), - ), - ); - for i in &hint_indices { - let i = *i + text.len() + 1; - text_layout = text_layout.range_attribute( - i..i + 1, - TextAttribute::TextColor(focus_color.clone()), - ); - text_layout = text_layout.range_attribute( - i..i + 1, - TextAttribute::Weight(FontWeight::BOLD), - ); - } - } - - let text_layout = text_layout.build().unwrap(); - let x = svg_x + 5.0; - let y = line_height * line as f64 + text_layout.y_offset(line_height); - let point = Point::new(x, y); - ctx.draw_text(&text_layout, point); - } - - fn file_paint_symbols( - path: &Path, - indices: &[usize], - workspace_path: Option<&Path>, - name: &str, - kind: SymbolKind, - ) -> (Option, String, Vec, String, Vec) { - let text = name.to_string(); - let hint = path.to_string_lossy(); - // Remove the workspace prefix from the path - let hint = workspace_path - .and_then(Path::to_str) - .and_then(|x| hint.strip_prefix(x)) - .map(|x| x.strip_prefix('/').unwrap_or(x)) - .map(ToString::to_string) - .unwrap_or_else(|| hint.to_string()); - let text_indices = indices - .iter() - .filter_map(|i| { - let i = *i; - if i < text.len() { - Some(i) - } else { - None - } - }) - .collect(); - let hint_indices = indices - .iter() - .filter_map(|i| { - let i = *i; - if i >= text.len() { - Some(i - text.len()) - } else { - None - } - }) - .collect(); - (symbol_svg(&kind), text, text_indices, hint, hint_indices) - } - - fn file_paint_items( - path: &Path, - indices: &[usize], - ) -> (Option, String, Vec, String, Vec) { - let (svg, _) = file_svg(path); - let file_name = path - .file_name() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_string(); - let folder = path - .parent() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_string(); - let folder_len = folder.len(); - let text_indices: Vec = indices - .iter() - .filter_map(|i| { - let i = *i; - if folder_len > 0 { - if i > folder_len { - Some(i - folder_len - 1) - } else { - None - } - } else { - Some(i) - } - }) - .collect(); - let hint_indices: Vec = indices - .iter() - .filter_map(|i| { - let i = *i; - if i < folder_len { - Some(i) - } else { - None - } - }) - .collect(); - (Some(svg), file_name, text_indices, folder, hint_indices) - } -} - -impl Default for PaletteContent { - fn default() -> Self { - Self::new() - } -} - -impl Widget for PaletteContent { - fn event( - &mut self, - ctx: &mut EventCtx, - event: &Event, - data: &mut PaletteViewData, - _env: &Env, - ) { - match event { - Event::MouseMove(_mouse_event) => { - ctx.set_cursor(&druid::Cursor::Pointer); - } - Event::MouseDown(mouse_event) => { - let line = (mouse_event.pos.y / self.line_height).floor() as usize; - self.mouse_down = line; - ctx.set_handled(); - } - Event::MouseUp(mouse_event) => { - let line = (mouse_event.pos.y / self.line_height).floor() as usize; - if line == self.mouse_down { - let palette = Arc::make_mut(&mut data.palette); - palette.index = line; - data.select(ctx); - } - ctx.set_handled(); - } - _ => (), - } - } - - fn lifecycle( - &mut self, - _ctx: &mut LifeCycleCtx, - _event: &LifeCycle, - _data: &PaletteViewData, - _env: &Env, - ) { - } - - fn update( - &mut self, - _ctx: &mut UpdateCtx, - _old_data: &PaletteViewData, - _data: &PaletteViewData, - _env: &Env, - ) { - } - - fn layout( - &mut self, - _ctx: &mut LayoutCtx, - bc: &BoxConstraints, - data: &PaletteViewData, - _env: &Env, - ) -> Size { - let height = self.line_height * data.palette.len() as f64; - Size::new(bc.max().width, height) - } - - fn paint(&mut self, ctx: &mut PaintCtx, data: &PaletteViewData, _env: &Env) { - let rect = ctx.region().bounding_box(); - let size = ctx.size(); - - let items = data.palette.current_items(); - - let start_line = (rect.y0 / self.line_height).floor() as usize; - let end_line = (rect.y1 / self.line_height).ceil() as usize; - - let workspace_path = data.workspace.path.as_deref(); - for line in start_line..end_line { - if line >= items.len() { - break; - } - if line == data.palette.index { - ctx.fill( - Rect::ZERO - .with_origin(Point::new(0.0, line as f64 * self.line_height)) - .with_size(Size::new(size.width, self.line_height)), - data.config.get_color_unchecked(LapceTheme::PALETTE_CURRENT), - ); - } - - let item = &items[line]; - - Self::paint_palette_item( - &item.content, - ctx, - workspace_path, - line, - &item.indices, - self.line_height, - &data.config, - ); - } - } -} - pub struct PalettePreview {} impl PalettePreview { @@ -1016,3 +617,340 @@ fn layout( fn paint(&mut self, _ctx: &mut PaintCtx, _data: &PaletteViewData, _env: &Env) {} } + +struct PaletteItemPaintInfo { + svg: Option, + text: String, + text_indices: Vec, + hint: String, + hint_indices: Vec, +} +impl PaletteItemPaintInfo { + /// Construct paint info when there is only known text and text indices + fn new_text(text: String, text_indices: Vec) -> PaletteItemPaintInfo { + PaletteItemPaintInfo { + svg: None, + text, + text_indices, + hint: String::new(), + hint_indices: Vec::new(), + } + } +} + +impl ListPaint for PaletteItem { + fn paint( + &self, + ctx: &mut PaintCtx, + data: &ListData, + _env: &Env, + line: usize, + ) { + let PaletteItemPaintInfo { + svg, + text, + text_indices, + hint, + hint_indices, + } = match &self.content { + PaletteItemContent::File(path, _) => { + file_paint_items(path, &self.indices) + } + PaletteItemContent::DocumentSymbol { + kind, + name, + container_name, + .. + } => { + let text = name.to_string(); + let hint = container_name.clone().unwrap_or_else(|| "".to_string()); + let text_indices = self + .indices + .iter() + .filter_map(|i| { + let i = *i; + if i < text.len() { + Some(i) + } else { + None + } + }) + .collect(); + let hint_indices = self + .indices + .iter() + .filter_map(|i| { + let i = *i; + if i >= text.len() { + Some(i - text.len()) + } else { + None + } + }) + .collect(); + PaletteItemPaintInfo { + svg: symbol_svg(kind), + text, + text_indices, + hint, + hint_indices, + } + } + PaletteItemContent::WorkspaceSymbol { + kind, + name, + location, + .. + } => file_paint_symbols( + &location.path, + &self.indices, + data.data + .workspace + .as_ref() + .and_then(|workspace| workspace.path.as_deref()), + name.as_str(), + *kind, + ), + PaletteItemContent::Line(_, text) => { + PaletteItemPaintInfo::new_text(text.clone(), self.indices.to_vec()) + } + PaletteItemContent::ReferenceLocation(rel_path, _location) => { + file_paint_items(rel_path, &self.indices) + } + PaletteItemContent::Workspace(w) => { + let text = w.path.as_ref().unwrap().to_str().unwrap(); + let text = match &w.kind { + LapceWorkspaceType::Local => text.to_string(), + LapceWorkspaceType::RemoteSSH(user, host) => { + format!("[{user}@{host}] {text}") + } + LapceWorkspaceType::RemoteWSL => { + format!("[wsl] {text}") + } + }; + PaletteItemPaintInfo::new_text(text, self.indices.to_vec()) + } + PaletteItemContent::Command(command) => { + let text = command + .kind + .desc() + .map(|m| m.to_string()) + .unwrap_or_else(|| "".to_string()); + PaletteItemPaintInfo::new_text(text, self.indices.to_vec()) + } + PaletteItemContent::Theme(theme) => PaletteItemPaintInfo::new_text( + theme.to_string(), + self.indices.to_vec(), + ), + PaletteItemContent::Language(name) => PaletteItemPaintInfo::new_text( + name.to_string(), + self.indices.to_vec(), + ), + PaletteItemContent::TerminalLine(_line, content) => { + PaletteItemPaintInfo::new_text( + content.clone(), + self.indices.to_vec(), + ) + } + PaletteItemContent::SshHost(user, host) => { + PaletteItemPaintInfo::new_text( + format!("{user}@{host}"), + self.indices.to_vec(), + ) + } + }; + + let line_height = data.line_height() as f64; + + if let Some(svg) = svg.as_ref() { + let width = 14.0; + let height = 14.0; + let rect = Size::new(width, height).to_rect().with_origin(Point::new( + (line_height - width) / 2.0 + 5.0, + (line_height - height) / 2.0 + line_height * line as f64, + )); + ctx.draw_svg(svg, rect, None); + } + + let svg_x = match &self.content { + &PaletteItemContent::Line(_, _) | &PaletteItemContent::Workspace(_) => { + 0.0 + } + _ => line_height, + }; + + let focus_color = data.config.get_color_unchecked(LapceTheme::EDITOR_FOCUS); + + let full_text = if hint.is_empty() { + text.clone() + } else { + text.clone() + " " + &hint + }; + let mut text_layout = ctx + .text() + .new_text_layout(full_text.clone()) + .font( + data.config.ui.font_family(), + data.config.ui.font_size() as f64, + ) + .text_color( + data.config + .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) + .clone(), + ); + for &i_start in &text_indices { + let i_end = full_text + .char_indices() + .find(|(i, _)| *i == i_start) + .map(|(_, c)| c.len_utf8() + i_start); + let i_end = if let Some(i_end) = i_end { + i_end + } else { + // Log a warning, but continue as we don't want to crash on a bug + log::warn!( + "Invalid text indices in palette: text: '{}', i_start: {}", + text, + i_start + ); + continue; + }; + + text_layout = text_layout.range_attribute( + i_start..i_end, + TextAttribute::TextColor(focus_color.clone()), + ); + text_layout = text_layout.range_attribute( + i_start..i_end, + TextAttribute::Weight(FontWeight::BOLD), + ); + } + + if !hint.is_empty() { + text_layout = text_layout + .range_attribute( + text.len() + 1..full_text.len(), + TextAttribute::FontSize(13.0), + ) + .range_attribute( + text.len() + 1..full_text.len(), + TextAttribute::TextColor( + data.config + .get_color_unchecked(LapceTheme::EDITOR_DIM) + .clone(), + ), + ); + for i in &hint_indices { + let i = *i + text.len() + 1; + text_layout = text_layout.range_attribute( + i..i + 1, + TextAttribute::TextColor(focus_color.clone()), + ); + text_layout = text_layout.range_attribute( + i..i + 1, + TextAttribute::Weight(FontWeight::BOLD), + ); + } + } + + let text_layout = text_layout.build().unwrap(); + let x = svg_x + 5.0; + let y = line_height * line as f64 + text_layout.y_offset(line_height); + let point = Point::new(x, y); + ctx.draw_text(&text_layout, point); + } +} + +fn file_paint_symbols( + path: &Path, + indices: &[usize], + workspace_path: Option<&Path>, + name: &str, + kind: SymbolKind, +) -> PaletteItemPaintInfo { + let text = name.to_string(); + let hint = path.to_string_lossy(); + // Remove the workspace prefix from the path + let hint = workspace_path + .and_then(Path::to_str) + .and_then(|x| hint.strip_prefix(x)) + .map(|x| x.strip_prefix('/').unwrap_or(x)) + .map(ToString::to_string) + .unwrap_or_else(|| hint.to_string()); + let text_indices = indices + .iter() + .filter_map(|i| { + let i = *i; + if i < text.len() { + Some(i) + } else { + None + } + }) + .collect(); + let hint_indices = indices + .iter() + .filter_map(|i| { + let i = *i; + if i >= text.len() { + Some(i - text.len()) + } else { + None + } + }) + .collect(); + PaletteItemPaintInfo { + svg: symbol_svg(&kind), + text, + text_indices, + hint, + hint_indices, + } +} + +fn file_paint_items(path: &Path, indices: &[usize]) -> PaletteItemPaintInfo { + let (svg, _) = file_svg(path); + let file_name = path + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + let folder = path + .parent() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_string(); + let folder_len = folder.len(); + let text_indices: Vec = indices + .iter() + .filter_map(|i| { + let i = *i; + if folder_len > 0 { + if i > folder_len { + Some(i - folder_len - 1) + } else { + None + } + } else { + Some(i) + } + }) + .collect(); + let hint_indices: Vec = indices + .iter() + .filter_map(|i| { + let i = *i; + if i < folder_len { + Some(i) + } else { + None + } + }) + .collect(); + PaletteItemPaintInfo { + svg: Some(svg), + text: file_name, + text_indices, + hint: folder, + hint_indices, + } +} diff --git a/lapce-ui/src/tab.rs b/lapce-ui/src/tab.rs index 58e12917..6b8c54ab 100644 --- a/lapce-ui/src/tab.rs +++ b/lapce-ui/src/tab.rs @@ -76,7 +76,7 @@ pub struct LapceTab { id: WidgetId, pub title: WidgetPod>>, main_split: WidgetPod>>, - completion: WidgetPod>>, + completion: WidgetPod, hover: WidgetPod>>, rename: WidgetPod>>, status: WidgetPod>>, @@ -179,7 +179,7 @@ pub fn new(data: &mut LapceTabData) -> Self { id: data.id, title, main_split: WidgetPod::new(main_split.boxed()), - completion: WidgetPod::new(completion.boxed()), + completion: WidgetPod::new(completion), hover: WidgetPod::new(hover.boxed()), rename: WidgetPod::new(rename.boxed()), picker: WidgetPod::new(picker.boxed()), @@ -2197,9 +2197,13 @@ fn layout( .set_origin(ctx, data, env, main_split_origin); if data.completion.status != CompletionStatus::Inactive { - let completion_origin = - data.completion_origin(ctx.text(), self_size, &data.config); - self.completion.layout(ctx, bc, data, env); + let completion_size = self.completion.layout(ctx, bc, data, env); + let completion_origin = data.completion_origin( + ctx.text(), + self_size, + completion_size, + &data.config, + ); self.completion .set_origin(ctx, data, env, completion_origin); }