From dfd68d4cb8f24818ee850409806a18dc2fe2e57a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 13 Nov 2023 20:38:37 +0100 Subject: [PATCH 001/205] WIP: start search2 --- Cargo.lock | 30 + Cargo.toml | 1 + crates/editor2/src/items.rs | 39 +- crates/search2/Cargo.toml | 40 + crates/search2/src/buffer_search.rs | 1797 +++++++++++++++++ crates/search2/src/history.rs | 184 ++ crates/search2/src/mode.rs | 65 + crates/search2/src/project_search.rs | 2661 ++++++++++++++++++++++++++ crates/search2/src/search.rs | 115 ++ crates/search2/src/search_bar.rs | 177 ++ crates/ui2/src/components/icon.rs | 4 + crates/workspace2/src/pane.rs | 2 +- crates/workspace2/src/searchable.rs | 23 +- crates/workspace2/src/toolbar.rs | 6 +- crates/workspace2/src/workspace2.rs | 2 +- crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- crates/zed2/src/zed2.rs | 8 +- 18 files changed, 5115 insertions(+), 43 deletions(-) create mode 100644 crates/search2/Cargo.toml create mode 100644 crates/search2/src/buffer_search.rs create mode 100644 crates/search2/src/history.rs create mode 100644 crates/search2/src/mode.rs create mode 100644 crates/search2/src/project_search.rs create mode 100644 crates/search2/src/search.rs create mode 100644 crates/search2/src/search_bar.rs diff --git a/Cargo.lock b/Cargo.lock index 49f37fb042..2cae47a029 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7855,6 +7855,35 @@ dependencies = [ "workspace", ] +[[package]] +name = "search2" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "client2", + "collections", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "log", + "menu2", + "postage", + "project2", + "serde", + "serde_derive", + "serde_json", + "settings2", + "smallvec", + "smol", + "theme2", + "ui2", + "unindent", + "util", + "workspace2", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -11425,6 +11454,7 @@ dependencies = [ "rsa 0.4.0", "rust-embed", "schemars", + "search2", "serde", "serde_derive", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 905750f835..958db9b7bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ members = [ "crates/rpc", "crates/rpc2", "crates/search", + "crates/search2", "crates/settings", "crates/settings2", "crates/snippet", diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 25e9f91608..6b396278b6 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -906,17 +906,16 @@ impl SearchableItem for Editor { type Match = Range; fn clear_matches(&mut self, cx: &mut ViewContext) { - todo!() - // self.clear_background_highlights::(cx); + self.clear_background_highlights::(cx); } fn update_matches(&mut self, matches: Vec>, cx: &mut ViewContext) { - todo!() - // self.highlight_background::( - // matches, - // |theme| theme.search.match_background, - // cx, - // ); + dbg!(&matches); + self.highlight_background::( + matches, + |theme| theme.title_bar_background, // todo: update theme + cx, + ); } fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { @@ -951,22 +950,20 @@ impl SearchableItem for Editor { matches: Vec>, cx: &mut ViewContext, ) { - todo!() - // self.unfold_ranges([matches[index].clone()], false, true, cx); - // let range = self.range_for_match(&matches[index]); - // self.change_selections(Some(Autoscroll::fit()), cx, |s| { - // s.select_ranges([range]); - // }) + self.unfold_ranges([matches[index].clone()], false, true, cx); + let range = self.range_for_match(&matches[index]); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }) } fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - todo!() - // self.unfold_ranges(matches.clone(), false, false, cx); - // let mut ranges = Vec::new(); - // for m in &matches { - // ranges.push(self.range_for_match(&m)) - // } - // self.change_selections(None, cx, |s| s.select_ranges(ranges)); + self.unfold_ranges(matches.clone(), false, false, cx); + let mut ranges = Vec::new(); + for m in &matches { + ranges.push(self.range_for_match(&m)) + } + self.change_selections(None, cx, |s| s.select_ranges(ranges)); } fn replace( &mut self, diff --git a/crates/search2/Cargo.toml b/crates/search2/Cargo.toml new file mode 100644 index 0000000000..97cfdd6494 --- /dev/null +++ b/crates/search2/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "search2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/search.rs" +doctest = false + +[dependencies] +bitflags = "1" +collections = { path = "../collections" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +util = { path = "../util" } +ui = {package = "ui2", path = "../ui2"} +workspace = { package = "workspace2", path = "../workspace2" } +#semantic_index = { path = "../semantic_index" } +anyhow.workspace = true +futures.workspace = true +log.workspace = true +postage.workspace = true +serde.workspace = true +serde_derive.workspace = true +smallvec.workspace = true +smol.workspace = true +serde_json.workspace = true +[dev-dependencies] +client = { package = "client2", path = "../client2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } + +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +unindent.workspace = true diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs new file mode 100644 index 0000000000..fdd03f71c3 --- /dev/null +++ b/crates/search2/src/buffer_search.rs @@ -0,0 +1,1797 @@ +use crate::{ + history::SearchHistory, + mode::{next_mode, SearchMode, Side}, + search_bar::{render_nav_button, render_search_mode_button}, + CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, + SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, + ToggleWholeWord, +}; +use collections::HashMap; +use editor::Editor; +use futures::channel::oneshot; +use gpui::{ + action, actions, div, Action, AnyElement, AnyView, AppContext, Component, Div, Entity, + EventEmitter, ParentElement as _, Render, Subscription, Svg, Task, View, ViewContext, + VisualContext as _, WindowContext, +}; +use project::search::SearchQuery; +use serde::Deserialize; +use std::{any::Any, sync::Arc}; +use theme::ActiveTheme; + +use ui::{IconButton, Label}; +use util::ResultExt; +use workspace::{ + item::ItemHandle, + searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, + Pane, ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +#[action] +pub struct Deploy { + pub focus: bool, +} + +actions!(Dismiss, FocusEditor); + +pub enum Event { + UpdateLocation, +} + +pub fn init(cx: &mut AppContext) { + dbg!("Registered"); + cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) + .detach(); +} + +pub struct BufferSearchBar { + query_editor: View, + replacement_editor: View, + active_searchable_item: Option>, + active_match_index: Option, + active_searchable_item_subscription: Option, + active_search: Option>, + searchable_items_with_matches: + HashMap, Vec>>, + pending_search: Option>, + search_options: SearchOptions, + default_options: SearchOptions, + query_contains_error: bool, + dismissed: bool, + search_history: SearchHistory, + current_mode: SearchMode, + replace_enabled: bool, +} + +impl EventEmitter for BufferSearchBar {} +impl EventEmitter for BufferSearchBar {} +impl Render for BufferSearchBar { + // fn ui_name() -> &'static str { + // "BufferSearchBar" + // } + + // fn update_keymap_context( + // &self, + // keymap: &mut gpui::keymap_matcher::KeymapContext, + // cx: &AppContext, + // ) { + // Self::reset_to_default_keymap_context(keymap); + // let in_replace = self + // .replacement_editor + // .read_with(cx, |_, cx| cx.is_self_focused()) + // .unwrap_or(false); + // if in_replace { + // keymap.add_identifier("in_replace"); + // } + // } + + // fn focus_in(&mut self, _: View, cx: &mut ViewContext) { + // cx.focus(&self.query_editor); + // } + type Element = Div; + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let theme = cx.theme().clone(); + // let query_container_style = if self.query_contains_error { + // theme.search.invalid_editor + // } else { + // theme.search.editor.input.container + // }; + let supported_options = self + .active_searchable_item + .as_ref() + .map(|active_searchable_item| active_searchable_item.supported_options()) + .unwrap_or_default(); + + // let previous_query_keystrokes = + // cx.binding_for_action(&PreviousHistoryQuery {}) + // .map(|binding| { + // binding + // .keystrokes() + // .iter() + // .map(|k| k.to_string()) + // .collect::>() + // }); + // let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { + // binding + // .keystrokes() + // .iter() + // .map(|k| k.to_string()) + // .collect::>() + // }); + // let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + // (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + // format!( + // "Search ({}/{} for previous/next query)", + // previous_query_keystrokes.join(" "), + // next_query_keystrokes.join(" ") + // ) + // } + // (None, Some(next_query_keystrokes)) => { + // format!( + // "Search ({} for next query)", + // next_query_keystrokes.join(" ") + // ) + // } + // (Some(previous_query_keystrokes), None) => { + // format!( + // "Search ({} for previous query)", + // previous_query_keystrokes.join(" ") + // ) + // } + // (None, None) => String::new(), + // }; + let new_placeholder_text = Arc::from("Fix this up!"); + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(new_placeholder_text, cx); + }); + self.replacement_editor.update(cx, |editor, cx| { + editor.set_placeholder_text("Replace with...", cx); + }); + div() + .child(self.query_editor.clone()) + .child(self.replacement_editor.clone()) + // let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + // let is_active = self.current_mode == mode; + + // render_search_mode_button( + // mode, + // side, + // is_active, + // move |_, this, cx| { + // this.activate_search_mode(mode, cx); + // }, + // cx, + // ) + // }; + // let search_option_button = |option| { + // let is_active = self.search_options.contains(option); + // option.as_button(is_active) + // }; + // let match_count = self + // .active_searchable_item + // .as_ref() + // .and_then(|searchable_item| { + // if self.query(cx).is_empty() { + // return None; + // } + // let matches = self + // .searchable_items_with_matches + // .get(&searchable_item.downgrade())?; + // let message = if let Some(match_ix) = self.active_match_index { + // format!("{}/{}", match_ix + 1, matches.len()) + // } else { + // "No matches".to_string() + // }; + + // Some( + // Label::new(message) + // .contained() + // .with_style(theme.search.match_index.container) + // .aligned(), + // ) + // }); + // let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + // render_nav_button( + // label, + // direction, + // self.active_match_index.is_some(), + // move |_, this, cx| match direction { + // Direction::Prev => this.select_prev_match(&Default::default(), cx), + // Direction::Next => this.select_next_match(&Default::default(), cx), + // }, + // cx, + // ) + // }; + // let query_column = Flex::row() + // .with_child( + // Svg::for_style(theme.search.editor_icon.clone().icon) + // .contained() + // .with_style(theme.search.editor_icon.clone().container), + // ) + // .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) + // .with_child( + // Flex::row() + // .with_children( + // supported_options + // .case + // .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)), + // ) + // .with_children( + // supported_options + // .word + // .then(|| search_option_button(SearchOptions::WHOLE_WORD)), + // ) + // .flex_float() + // .contained(), + // ) + // .align_children_center() + // .contained() + // .with_style(query_container_style) + // .constrained() + // .with_min_width(theme.search.editor.min_width) + // .with_max_width(theme.search.editor.max_width) + // .with_height(theme.search.search_bar_row_height) + // .flex(1., false); + // let should_show_replace_input = self.replace_enabled && supported_options.replacement; + + // let replacement = should_show_replace_input.then(|| { + // div() + // .child( + // Svg::for_style(theme.search.replace_icon.clone().icon) + // .contained() + // .with_style(theme.search.replace_icon.clone().container), + // ) + // .child(self.replacement_editor) + // .align_children_center() + // .flex(1., true) + // .contained() + // .with_style(query_container_style) + // .constrained() + // .with_min_width(theme.search.editor.min_width) + // .with_max_width(theme.search.editor.max_width) + // .with_height(theme.search.search_bar_row_height) + // .flex(1., false) + // }); + // let replace_all = + // should_show_replace_input.then(|| super::replace_action(ReplaceAll, "Replace all")); + // let replace_next = + // should_show_replace_input.then(|| super::replace_action(ReplaceNext, "Replace next")); + // let switches_column = supported_options.replacement.then(|| { + // Flex::row() + // .align_children_center() + // .with_child(super::toggle_replace_button(self.replace_enabled)) + // .constrained() + // .with_height(theme.search.search_bar_row_height) + // .contained() + // .with_style(theme.search.option_button_group) + // }); + // let mode_column = div() + // .child(search_button_for_mode( + // SearchMode::Text, + // Some(Side::Left), + // cx, + // )) + // .child(search_button_for_mode( + // SearchMode::Regex, + // Some(Side::Right), + // cx, + // )) + // .contained() + // .with_style(theme.search.modes_container) + // .constrained() + // .with_height(theme.search.search_bar_row_height); + + // let nav_column = div() + // .align_children_center() + // .with_children(replace_next) + // .with_children(replace_all) + // .with_child(self.render_action_button("icons/select-all.svg", cx)) + // .with_child(div().children(match_count)) + // .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + // .with_child(nav_button_for_direction(">", Direction::Next, cx)) + // .constrained() + // .with_height(theme.search.search_bar_row_height) + // .flex_float(); + + // div() + // .child(query_column) + // .child(mode_column) + // .children(switches_column) + // .children(replacement) + // .child(nav_column) + // .contained() + // .with_style(theme.search.container) + // .into_any_named("search bar") + } +} + +impl ToolbarItemView for BufferSearchBar { + fn set_active_pane_item( + &mut self, + item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + cx.notify(); + self.active_searchable_item_subscription.take(); + self.active_searchable_item.take(); + dbg!("Take?"); + self.pending_search.take(); + + if let Some(searchable_item_handle) = + item.and_then(|item| item.to_searchable_item_handle(cx)) + { + let this = cx.view().downgrade(); + self.active_searchable_item_subscription = + Some(searchable_item_handle.subscribe_to_search_events( + cx, + Box::new(move |search_event, cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| { + this.on_active_searchable_item_event(search_event, cx) + }); + } + }), + )); + + self.active_searchable_item = Some(searchable_item_handle); + let _ = self.update_matches(cx); + if !self.dismissed { + return ToolbarItemLocation::Secondary; + } + } + + ToolbarItemLocation::Hidden + } + + fn row_count(&self, _: &WindowContext<'_>) -> usize { + 1 + } +} + +impl BufferSearchBar { + pub fn register(workspace: &mut Workspace) { + workspace.register_action(|workspace, a: &Deploy, cx| { + dbg!("Setting"); + workspace.active_pane().update(cx, |this, cx| { + this.toolbar().update(cx, |this, cx| { + let view = cx.build_view(|cx| BufferSearchBar::new(cx)); + this.add_item(view.clone(), cx); + view.update(cx, |this, cx| this.deploy(a, cx)); + cx.notify(); + }) + }); + }); + } + pub fn new(cx: &mut ViewContext) -> Self { + dbg!("New"); + let query_editor = cx.build_view(|cx| Editor::single_line(cx)); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + let replacement_editor = cx.build_view(|cx| Editor::single_line(cx)); + cx.subscribe(&replacement_editor, Self::on_query_editor_event) + .detach(); + Self { + query_editor, + replacement_editor, + active_searchable_item: None, + active_searchable_item_subscription: None, + active_match_index: None, + searchable_items_with_matches: Default::default(), + default_options: SearchOptions::NONE, + search_options: SearchOptions::NONE, + pending_search: None, + query_contains_error: false, + dismissed: true, + search_history: SearchHistory::default(), + current_mode: SearchMode::default(), + active_search: None, + replace_enabled: false, + } + } + + pub fn is_dismissed(&self) -> bool { + self.dismissed + } + + pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { + self.dismissed = true; + dbg!("Dismissed :("); + for searchable_item in self.searchable_items_with_matches.keys() { + if let Some(searchable_item) = + WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) + { + searchable_item.clear_matches(cx); + } + } + if let Some(active_editor) = self.active_searchable_item.as_ref() { + let handle = active_editor.focus_handle(cx); + cx.focus(&handle); + } + cx.emit(Event::UpdateLocation); + cx.notify(); + } + + pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext) -> bool { + if self.show(cx) { + self.search_suggested(cx); + if deploy.focus { + self.select_query(cx); + let handle = cx.focus_handle(); + cx.focus(&handle); + } + return true; + } + + false + } + + pub fn show(&mut self, cx: &mut ViewContext) -> bool { + if self.active_searchable_item.is_none() { + dbg!("Hey"); + return false; + } + dbg!("not dismissed"); + self.dismissed = false; + cx.notify(); + cx.emit(Event::UpdateLocation); + true + } + + pub fn search_suggested(&mut self, cx: &mut ViewContext) { + let search = self + .query_suggestion(cx) + .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx)); + + if let Some(search) = search { + cx.spawn(|this, mut cx| async move { + search.await?; + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) + }) + .detach_and_log_err(cx); + } + } + + pub fn activate_current_match(&mut self, cx: &mut ViewContext) { + if let Some(match_ix) = self.active_match_index { + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&active_searchable_item.downgrade()) + { + active_searchable_item.activate_match(match_ix, matches, cx) + } + } + } + } + + pub fn select_query(&mut self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&Default::default(), cx); + }); + } + + pub fn query(&self, cx: &WindowContext) -> String { + self.query_editor.read(cx).text(cx) + } + pub fn replacement(&self, cx: &WindowContext) -> String { + self.replacement_editor.read(cx).text(cx) + } + pub fn query_suggestion(&mut self, cx: &mut ViewContext) -> Option { + self.active_searchable_item + .as_ref() + .map(|searchable_item| searchable_item.query_suggestion(cx)) + .filter(|suggestion| !suggestion.is_empty()) + } + + pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext) { + if replacement.is_none() { + self.replace_enabled = false; + return; + } + self.replace_enabled = true; + self.replacement_editor + .update(cx, |replacement_editor, cx| { + replacement_editor + .buffer() + .update(cx, |replacement_buffer, cx| { + let len = replacement_buffer.len(cx); + replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx); + }); + }); + } + + pub fn search( + &mut self, + query: &str, + options: Option, + cx: &mut ViewContext, + ) -> oneshot::Receiver<()> { + let options = options.unwrap_or(self.default_options); + if query != self.query(cx) || self.search_options != options { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.len(cx); + query_buffer.edit([(0..len, query)], None, cx); + }); + }); + self.search_options = options; + self.query_contains_error = false; + self.clear_matches(cx); + cx.notify(); + } + self.update_matches(cx) + } + + fn render_action_button( + &self, + icon: &'static str, + cx: &mut ViewContext, + ) -> impl Component { + let tooltip = "Select All Matches"; + let theme = cx.theme(); + // let tooltip_style = theme.tooltip.clone(); + + // let style = theme.search.action_button.clone(); + + IconButton::new(0, ui::Icon::SelectAll) + .on_click(|_: &mut Self, cx| cx.dispatch_action(Box::new(SelectAllMatches))) + } + + pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + assert_ne!( + mode, + SearchMode::Semantic, + "Semantic search is not supported in buffer search" + ); + if mode == self.current_mode { + return; + } + self.current_mode = mode; + let _ = self.update_matches(cx); + cx.notify(); + } + + fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { + let mut propagate_action = true; + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + if search_bar.deploy(action, cx) { + propagate_action = false; + } + }); + } + if !propagate_action { + cx.stop_propagation(); + } + } + + fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if !search_bar.read(cx).dismissed { + search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx)); + cx.stop_propagation(); + return; + } + } + } + + pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { + if let Some(active_editor) = self.active_searchable_item.as_ref() { + let handle = active_editor.focus_handle(cx); + cx.focus(&handle); + } + } + + fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext) { + self.search_options.toggle(search_option); + self.default_options = self.search_options; + let _ = self.update_matches(cx); + cx.notify(); + } + + pub fn set_search_options( + &mut self, + search_options: SearchOptions, + cx: &mut ViewContext, + ) { + self.search_options = search_options; + cx.notify(); + } + + fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { + self.select_match(Direction::Next, 1, cx); + } + + fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { + self.select_match(Direction::Prev, 1, cx); + } + + fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext) { + if !self.dismissed && self.active_match_index.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + searchable_item.select_matches(matches, cx); + self.focus_editor(&FocusEditor, cx); + } + } + } + } + + pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext) { + if let Some(index) = self.active_match_index { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let new_match_index = searchable_item + .match_index_for_direction(matches, index, direction, count, cx); + searchable_item.update_matches(matches, cx); + searchable_item.activate_match(new_match_index, matches, cx); + } + } + } + } + + pub fn select_last_match(&mut self, cx: &mut ViewContext) { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if matches.len() == 0 { + return; + } + let new_match_index = matches.len() - 1; + searchable_item.update_matches(matches, cx); + searchable_item.activate_match(new_match_index, matches, cx); + } + } + } + + fn select_next_match_on_pane( + pane: &mut Pane, + action: &SelectNextMatch, + cx: &mut ViewContext, + ) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx)); + } + } + + fn select_prev_match_on_pane( + pane: &mut Pane, + action: &SelectPrevMatch, + cx: &mut ViewContext, + ) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx)); + } + } + + fn select_all_matches_on_pane( + pane: &mut Pane, + action: &SelectAllMatches, + cx: &mut ViewContext, + ) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx)); + } + } + + fn on_query_editor_event( + &mut self, + _: View, + event: &editor::Event, + cx: &mut ViewContext, + ) { + if let editor::Event::Edited { .. } = event { + self.query_contains_error = false; + self.clear_matches(cx); + let search = self.update_matches(cx); + cx.spawn(|this, mut cx| async move { + search.await?; + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) + }) + .detach_and_log_err(cx); + } + } + + fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext) { + match event { + SearchEvent::MatchesInvalidated => { + let _ = self.update_matches(cx); + } + SearchEvent::ActiveMatchChanged => self.update_match_index(cx), + } + } + + fn clear_matches(&mut self, cx: &mut ViewContext) { + let mut active_item_matches = None; + for (searchable_item, matches) in self.searchable_items_with_matches.drain() { + if let Some(searchable_item) = + WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) + { + if Some(&searchable_item) == self.active_searchable_item.as_ref() { + active_item_matches = Some((searchable_item.downgrade(), matches)); + } else { + searchable_item.clear_matches(cx); + } + } + } + + self.searchable_items_with_matches + .extend(active_item_matches); + } + + fn update_matches(&mut self, cx: &mut ViewContext) -> oneshot::Receiver<()> { + let (done_tx, done_rx) = oneshot::channel(); + let query = self.query(cx); + self.pending_search.take(); + dbg!("update_matches"); + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { + if query.is_empty() { + self.active_match_index.take(); + active_searchable_item.clear_matches(cx); + let _ = done_tx.send(()); + cx.notify(); + } else { + let query: Arc<_> = if self.current_mode == SearchMode::Regex { + match SearchQuery::regex( + query, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + Vec::new(), + Vec::new(), + ) { + Ok(query) => query.with_replacement(self.replacement(cx)), + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return done_rx; + } + } + } else { + match SearchQuery::text( + query, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + Vec::new(), + Vec::new(), + ) { + Ok(query) => query.with_replacement(self.replacement(cx)), + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return done_rx; + } + } + } + .into(); + self.active_search = Some(query.clone()); + let query_text = query.as_str().to_string(); + dbg!(&query_text); + let matches = active_searchable_item.find_matches(query, cx); + + let active_searchable_item = active_searchable_item.downgrade(); + self.pending_search = Some(cx.spawn(|this, mut cx| async move { + let matches = matches.await; + //dbg!(&matches); + this.update(&mut cx, |this, cx| { + dbg!("Updating!!"); + if let Some(active_searchable_item) = + WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) + { + dbg!("in if!!"); + this.searchable_items_with_matches + .insert(active_searchable_item.downgrade(), matches); + + this.update_match_index(cx); + this.search_history.add(query_text); + if !this.dismissed { + dbg!("Not dismissed"); + let matches = this + .searchable_items_with_matches + .get(&active_searchable_item.downgrade()) + .unwrap(); + active_searchable_item.update_matches(matches, cx); + let _ = done_tx.send(()); + } + cx.notify(); + } + }) + .log_err(); + })); + } + } + done_rx + } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + let new_index = self + .active_searchable_item + .as_ref() + .and_then(|searchable_item| { + let matches = self + .searchable_items_with_matches + .get(&searchable_item.downgrade())?; + searchable_item.active_match_index(matches, cx) + }); + if new_index != self.active_match_index { + self.active_match_index = new_index; + cx.notify(); + } + } + + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { + if let Some(new_query) = self.search_history.next().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + } else { + self.search_history.reset_selection(); + let _ = self.search("", Some(self.search_options), cx); + } + } + + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { + if self.query(cx).is_empty() { + if let Some(new_query) = self.search_history.current().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + return; + } + } + + if let Some(new_query) = self.search_history.previous().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + } + } + fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { + self.activate_search_mode(next_mode(&self.current_mode, false), cx); + } + fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| { + if bar.show(cx) { + should_propagate = false; + bar.cycle_mode(action, cx); + false + } else { + true + } + }); + } + if !should_propagate { + cx.stop_propagation(); + } + } + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { + if let Some(_) = &self.active_searchable_item { + self.replace_enabled = !self.replace_enabled; + if !self.replace_enabled { + let handle = self.query_editor.focus_handle(cx); + cx.focus(&handle); + } + cx.notify(); + } + } + fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| { + if let Some(_) = &bar.active_searchable_item { + should_propagate = false; + bar.replace_enabled = !bar.replace_enabled; + if bar.dismissed { + bar.show(cx); + } + if !bar.replace_enabled { + let handle = bar.query_editor.focus_handle(cx); + cx.focus(&handle); + } + cx.notify(); + } + }); + } + if !should_propagate { + cx.stop_propagation(); + } + } + fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { + let mut should_propagate = true; + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if let Some(active_index) = self.active_match_index { + let query = query + .as_ref() + .clone() + .with_replacement(self.replacement(cx)); + searchable_item.replace(&matches[active_index], &query, cx); + self.select_next_match(&SelectNextMatch, cx); + } + should_propagate = false; + self.focus_editor(&FocusEditor, cx); + } + } + } + } + if !should_propagate { + cx.stop_propagation(); + } + } + pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let query = query + .as_ref() + .clone() + .with_replacement(self.replacement(cx)); + for m in matches { + searchable_item.replace(m, &query, cx); + } + } + } + } + } + } + fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.replace_next(action, cx)); + cx.stop_propagation(); + return; + } + } + fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.replace_all(action, cx)); + cx.stop_propagation(); + return; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use editor::{DisplayPoint, Editor}; + use gpui::{color::Color, test::EmptyView, TestAppContext}; + use language::Buffer; + use unindent::Unindent as _; + + fn init_test(cx: &mut TestAppContext) -> (ViewHandle, ViewHandle) { + crate::project_search::tests::init_test(cx); + + let buffer = cx.add_model(|cx| { + Buffer::new( + 0, + cx.model_id() as u64, + r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + ) + }); + let window = cx.add_window(|_| EmptyView); + let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = window.add_view(cx, |cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + (editor, search_bar) + } + + #[gpui::test] + async fn test_search_simple(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + + // Search for a string that appears with different casing. + // By default, search is case-insensitive. + search_bar + .update(cx, |search_bar, cx| search_bar.search("us", None, cx)) + .await + .unwrap(); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_text_background_highlights(cx), + &[ + ( + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + Color::red(), + ), + ( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + ), + ] + ); + }); + + // Switch to a case sensitive search. + search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_text_background_highlights(cx), + &[( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + )] + ); + }); + + // Search for a string that appears both as a whole word and + // within other words. By default, all results are found. + search_bar + .update(cx, |search_bar, cx| search_bar.search("or", None, cx)) + .await + .unwrap(); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_text_background_highlights(cx), + &[ + ( + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), + Color::red(), + ), + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), + Color::red(), + ), + ( + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ( + DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), + Color::red(), + ), + ] + ); + }); + + // Switch to a whole word search. + search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_text_background_highlights(cx), + &[ + ( + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + Color::red(), + ), + ( + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + Color::red(), + ), + ( + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + Color::red(), + ), + ] + ); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the previous match selects + // the closest match to the left. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the next match selects the + // closest match to the right. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); + }); + + // Park the cursor after the last match and ensure that going to the previous match selects + // the last match. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); + }); + + // Park the cursor after the last match and ensure that going to the next match selects the + // first match. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + // Park the cursor before the first match and ensure that going to the previous match + // selects the last match. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + search_bar.read_with(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); + }); + } + + #[gpui::test] + async fn test_search_option_handling(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + + // show with options should make current search case sensitive + search_bar + .update(cx, |search_bar, cx| { + search_bar.show(cx); + search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_text_background_highlights(cx), + &[( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + )] + ); + }); + + // search_suggested should restore default options + search_bar.update(cx, |search_bar, cx| { + search_bar.search_suggested(cx); + assert_eq!(search_bar.search_options, SearchOptions::NONE) + }); + + // toggling a search option should update the defaults + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_text_background_highlights(cx), + &[( + DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), + Color::red(), + ),] + ); + }); + + // defaults should still include whole word + search_bar.update(cx, |search_bar, cx| { + search_bar.search_suggested(cx); + assert_eq!( + search_bar.search_options, + SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD + ) + }); + } + + #[gpui::test] + async fn test_search_select_all_matches(cx: &mut TestAppContext) { + crate::project_search::tests::init_test(cx); + + let buffer_text = r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(); + let expected_query_matches_count = buffer_text + .chars() + .filter(|c| c.to_ascii_lowercase() == 'a') + .count(); + assert!( + expected_query_matches_count > 1, + "Should pick a query with multiple results" + ); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); + let window = cx.add_window(|_| EmptyView); + let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = window.add_view(cx, |cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + search_bar + .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + cx.focus(search_bar.query_editor.as_any()); + search_bar.activate_current_match(cx); + }); + + window.read_with(cx, |cx| { + assert!( + !editor.is_focused(cx), + "Initially, the editor should not be focused" + ); + }); + + let initial_selections = editor.update(cx, |editor, cx| { + let initial_selections = editor.selections.display_ranges(cx); + assert_eq!( + initial_selections.len(), 1, + "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", + ); + initial_selections + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + search_bar.update(cx, |search_bar, cx| { + cx.focus(search_bar.query_editor.as_any()); + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + window.read_with(cx, |cx| { + assert!( + editor.is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + }); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should not change after selecting all matches" + ); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_next_match(&SelectNextMatch, cx); + }); + window.read_with(cx, |cx| { + assert!( + editor.is_focused(cx), + "Should still have editor focused after SelectNextMatch" + ); + }); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On next match, should deselect items and select the next match" + ); + assert_ne!( + all_selections, initial_selections, + "Next match should be different from the first selection" + ); + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should be updated to the next one" + ); + }); + + search_bar.update(cx, |search_bar, cx| { + cx.focus(search_bar.query_editor.as_any()); + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + window.read_with(cx, |cx| { + assert!( + editor.is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + }); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should not change after selecting all matches" + ); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + }); + window.read_with(cx, |cx| { + assert!( + editor.is_focused(cx), + "Should still have editor focused after SelectPrevMatch" + ); + }); + let last_match_selections = search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On previous match, should deselect items and select the previous item" + ); + assert_eq!( + all_selections, initial_selections, + "Previous match should be the same as the first selection" + ); + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should be updated to the previous one" + ); + all_selections + }); + + search_bar + .update(cx, |search_bar, cx| { + cx.focus(search_bar.query_editor.as_any()); + search_bar.search("abas_nonexistent_match", None, cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + window.read_with(cx, |cx| { + assert!( + !editor.is_focused(cx), + "Should not switch focus to editor if SelectAllMatches does not find any matches" + ); + }); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections, last_match_selections, + "Should not select anything new if there are no matches" + ); + assert!( + search_bar.active_match_index.is_none(), + "For no matches, there should be no active match index" + ); + }); + } + + #[gpui::test] + async fn test_search_query_history(cx: &mut TestAppContext) { + crate::project_search::tests::init_test(cx); + + let buffer_text = r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); + let window = cx.add_window(|_| EmptyView); + + let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = window.add_view(cx, |cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + // Add 3 search items into the history. + search_bar + .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) + .await + .unwrap(); + search_bar + .update(cx, |search_bar, cx| search_bar.search("b", None, cx)) + .await + .unwrap(); + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); + // Ensure that the latest search is active. + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next history query after the latest should set the query to the empty string. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // First previous query for empty current query should set the query to the latest. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Further previous items should go over the history in reverse order. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Previous items should never go behind the first history item. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "a"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "a"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next items should go over the history in the original order. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + search_bar + .update(cx, |search_bar, cx| search_bar.search("ba", None, cx)) + .await + .unwrap(); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "ba"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + + // New search input should add another entry to history and move the selection to the end of the history. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "ba"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + } + #[gpui::test] + async fn test_replace_simple(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("expression", None, cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally. + editor.set_text("expr$1", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex or regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + + // Search for word boundaries and replace just a single one. + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("banana", cx); + }); + search_bar.replace_next(&ReplaceNext, cx) + }); + // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Let's turn on regex mode. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("\\[([^\\]]+)\\]", None, cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("${1}number", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Now with a whole-word twist. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("things", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + // The only word affected by this edit should be `algorithms`, even though there's a bunch + // of words in this text that would match this regex if not for WHOLE_WORD. + assert_eq!( + editor.read_with(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching things + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + } +} diff --git a/crates/search2/src/history.rs b/crates/search2/src/history.rs new file mode 100644 index 0000000000..6b06c60293 --- /dev/null +++ b/crates/search2/src/history.rs @@ -0,0 +1,184 @@ +use smallvec::SmallVec; +const SEARCH_HISTORY_LIMIT: usize = 20; + +#[derive(Default, Debug, Clone)] +pub struct SearchHistory { + history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, + selected: Option, +} + +impl SearchHistory { + pub fn add(&mut self, search_string: String) { + if let Some(i) = self.selected { + if search_string == self.history[i] { + return; + } + } + + if let Some(previously_searched) = self.history.last_mut() { + if search_string.find(previously_searched.as_str()).is_some() { + *previously_searched = search_string; + self.selected = Some(self.history.len() - 1); + return; + } + } + + self.history.push(search_string); + if self.history.len() > SEARCH_HISTORY_LIMIT { + self.history.remove(0); + } + self.selected = Some(self.history.len() - 1); + } + + pub fn next(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let selected = self.selected?; + if selected == history_size - 1 { + return None; + } + let next_index = selected + 1; + self.selected = Some(next_index); + Some(&self.history[next_index]) + } + + pub fn current(&self) -> Option<&str> { + Some(&self.history[self.selected?]) + } + + pub fn previous(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let prev_index = match self.selected { + Some(selected_index) => { + if selected_index == 0 { + return None; + } else { + selected_index - 1 + } + } + None => history_size - 1, + }; + + self.selected = Some(prev_index); + Some(&self.history[prev_index]) + } + + pub fn reset_selection(&mut self) { + self.selected = None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.current(), + None, + "No current selection should be set fo the default search history" + ); + + search_history.add("rust".to_string()); + assert_eq!( + search_history.current(), + Some("rust"), + "Newly added item should be selected" + ); + + // check if duplicates are not added + search_history.add("rust".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should not add a duplicate" + ); + assert_eq!(search_history.current(), Some("rust")); + + // check if new string containing the previous string replaces it + search_history.add("rustlang".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should replace previous item if it's a substring" + ); + assert_eq!(search_history.current(), Some("rustlang")); + + // push enough items to test SEARCH_HISTORY_LIMIT + for i in 0..SEARCH_HISTORY_LIMIT * 2 { + search_history.add(format!("item{i}")); + } + assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); + } + + #[test] + fn test_next_and_previous() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.next(), + None, + "Default search history should not have a next item" + ); + + search_history.add("Rust".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("JavaScript".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("TypeScript".to_string()); + assert_eq!(search_history.next(), None); + + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.previous(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.previous(), Some("Rust")); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.previous(), None); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.next(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.next(), Some("TypeScript")); + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.next(), None); + assert_eq!(search_history.current(), Some("TypeScript")); + } + + #[test] + fn test_reset_selection() { + let mut search_history = SearchHistory::default(); + search_history.add("Rust".to_string()); + search_history.add("JavaScript".to_string()); + search_history.add("TypeScript".to_string()); + + assert_eq!(search_history.current(), Some("TypeScript")); + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + assert_eq!( + search_history.previous(), + Some("TypeScript"), + "Should start from the end after reset on previous item query" + ); + + search_history.previous(); + assert_eq!(search_history.current(), Some("JavaScript")); + search_history.previous(); + assert_eq!(search_history.current(), Some("Rust")); + + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + } +} diff --git a/crates/search2/src/mode.rs b/crates/search2/src/mode.rs new file mode 100644 index 0000000000..8afc2bd3f4 --- /dev/null +++ b/crates/search2/src/mode.rs @@ -0,0 +1,65 @@ +use gpui::Action; + +use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; +// TODO: Update the default search mode to get from config +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub enum SearchMode { + #[default] + Text, + Semantic, + Regex, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +pub(crate) enum Side { + Left, + Right, +} + +impl SearchMode { + pub(crate) fn label(&self) -> &'static str { + match self { + SearchMode::Text => "Text", + SearchMode::Semantic => "Semantic", + SearchMode::Regex => "Regex", + } + } + + pub(crate) fn region_id(&self) -> usize { + match self { + SearchMode::Text => 3, + SearchMode::Semantic => 4, + SearchMode::Regex => 5, + } + } + + pub(crate) fn tooltip_text(&self) -> &'static str { + match self { + SearchMode::Text => "Activate Text Search", + SearchMode::Semantic => "Activate Semantic Search", + SearchMode::Regex => "Activate Regex Search", + } + } + + pub(crate) fn activate_action(&self) -> Box { + match self { + SearchMode::Text => Box::new(ActivateTextMode), + SearchMode::Semantic => Box::new(ActivateSemanticMode), + SearchMode::Regex => Box::new(ActivateRegexMode), + } + } +} + +pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { + match mode { + SearchMode::Text => SearchMode::Regex, + SearchMode::Regex => { + if semantic_enabled { + SearchMode::Semantic + } else { + SearchMode::Text + } + } + SearchMode::Semantic => SearchMode::Text, + } +} diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs new file mode 100644 index 0000000000..f6e17bbee5 --- /dev/null +++ b/crates/search2/src/project_search.rs @@ -0,0 +1,2661 @@ +use crate::{ + history::SearchHistory, + mode::{SearchMode, Side}, + search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, + ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery, + PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, + ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, +}; +use anyhow::{Context, Result}; +use collections::HashMap; +use editor::{ + items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, + SelectAll, MAX_TAB_TITLE_LEN, +}; +use futures::StreamExt; +use gpui::{ + actions, + elements::*, + platform::{MouseButton, PromptLevel}, + Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, + Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, +}; +use menu::Confirm; +use project::{ + search::{SearchInputs, SearchQuery}, + Entry, Project, +}; +use semantic_index::{SemanticIndex, SemanticIndexStatus}; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + borrow::Cow, + collections::HashSet, + mem, + ops::{Not, Range}, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; +use util::{paths::PathMatcher, ResultExt as _}; +use workspace::{ + item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, + searchable::{Direction, SearchableItem, SearchableItemHandle}, + ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, +}; + +actions!( + project_search, + [SearchInNew, ToggleFocus, NextField, ToggleFilters,] +); + +#[derive(Default)] +struct ActiveSearches(HashMap, WeakViewHandle>); + +#[derive(Default)] +struct ActiveSettings(HashMap, ProjectSearchSettings>); + +pub fn init(cx: &mut AppContext) { + cx.set_global(ActiveSearches::default()); + cx.set_global(ActiveSettings::default()); + cx.add_action(ProjectSearchView::deploy); + cx.add_action(ProjectSearchView::move_focus_to_results); + cx.add_action(ProjectSearchBar::confirm); + cx.add_action(ProjectSearchBar::search_in_new); + cx.add_action(ProjectSearchBar::select_next_match); + cx.add_action(ProjectSearchBar::select_prev_match); + cx.add_action(ProjectSearchBar::replace_next); + cx.add_action(ProjectSearchBar::replace_all); + cx.add_action(ProjectSearchBar::cycle_mode); + cx.add_action(ProjectSearchBar::next_history_query); + cx.add_action(ProjectSearchBar::previous_history_query); + cx.add_action(ProjectSearchBar::activate_regex_mode); + cx.add_action(ProjectSearchBar::toggle_replace); + cx.add_action(ProjectSearchBar::toggle_replace_on_a_pane); + cx.add_action(ProjectSearchBar::activate_text_mode); + + // This action should only be registered if the semantic index is enabled + // We are registering it all the time, as I dont want to introduce a dependency + // for Semantic Index Settings globally whenever search is tested. + cx.add_action(ProjectSearchBar::activate_semantic_mode); + + cx.capture_action(ProjectSearchBar::tab); + cx.capture_action(ProjectSearchBar::tab_previous); + cx.capture_action(ProjectSearchView::replace_all); + cx.capture_action(ProjectSearchView::replace_next); + add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); + add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); + add_toggle_filters_action::(cx); +} + +fn add_toggle_filters_action(cx: &mut AppContext) { + cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) { + return; + } + } + cx.propagate_action(); + }); +} + +fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { + cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(option, cx) + }) { + return; + } + } + cx.propagate_action(); + }); +} + +struct ProjectSearch { + project: ModelHandle, + excerpts: ModelHandle, + pending_search: Option>>, + match_ranges: Vec>, + active_query: Option, + search_id: usize, + search_history: SearchHistory, + no_results: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum InputPanel { + Query, + Exclude, + Include, +} + +pub struct ProjectSearchView { + model: ModelHandle, + query_editor: ViewHandle, + replacement_editor: ViewHandle, + results_editor: ViewHandle, + semantic_state: Option, + semantic_permissioned: Option, + search_options: SearchOptions, + panels_with_errors: HashSet, + active_match_index: Option, + search_id: usize, + query_editor_was_focused: bool, + included_files_editor: ViewHandle, + excluded_files_editor: ViewHandle, + filters_enabled: bool, + replace_enabled: bool, + current_mode: SearchMode, +} + +struct SemanticState { + index_status: SemanticIndexStatus, + maintain_rate_limit: Option>, + _subscription: Subscription, +} + +#[derive(Debug, Clone)] +struct ProjectSearchSettings { + search_options: SearchOptions, + filters_enabled: bool, + current_mode: SearchMode, +} + +pub struct ProjectSearchBar { + active_project_search: Option>, + subscription: Option, +} + +impl Entity for ProjectSearch { + type Event = (); +} + +impl ProjectSearch { + fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { + let replica_id = project.read(cx).replica_id(); + Self { + project, + excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), + pending_search: Default::default(), + match_ranges: Default::default(), + active_query: None, + search_id: 0, + search_history: SearchHistory::default(), + no_results: None, + } + } + + fn clone(&self, cx: &mut ModelContext) -> ModelHandle { + cx.add_model(|cx| Self { + project: self.project.clone(), + excerpts: self + .excerpts + .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), + pending_search: Default::default(), + match_ranges: self.match_ranges.clone(), + active_query: self.active_query.clone(), + search_id: self.search_id, + search_history: self.search_history.clone(), + no_results: self.no_results.clone(), + }) + } + + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { + let search = self + .project + .update(cx, |project, cx| project.search(query.clone(), cx)); + self.search_id += 1; + self.search_history.add(query.as_str().to_string()); + self.active_query = Some(query); + self.match_ranges.clear(); + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let mut matches = search; + let this = this.upgrade(&cx)?; + this.update(&mut cx, |this, cx| { + this.match_ranges.clear(); + this.excerpts.update(cx, |this, cx| this.clear(cx)); + this.no_results = Some(true); + }); + + while let Some((buffer, anchors)) = matches.next().await { + let mut ranges = this.update(&mut cx, |this, cx| { + this.no_results = Some(false); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) + }) + }); + + while let Some(range) = ranges.next().await { + this.update(&mut cx, |this, _| this.match_ranges.push(range)); + } + this.update(&mut cx, |_, cx| cx.notify()); + } + + this.update(&mut cx, |this, cx| { + this.pending_search.take(); + cx.notify(); + }); + + None + })); + cx.notify(); + } + + fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext) { + let search = SemanticIndex::global(cx).map(|index| { + index.update(cx, |semantic_index, cx| { + semantic_index.search_project( + self.project.clone(), + inputs.as_str().to_owned(), + 10, + inputs.files_to_include().to_vec(), + inputs.files_to_exclude().to_vec(), + cx, + ) + }) + }); + self.search_id += 1; + self.match_ranges.clear(); + self.search_history.add(inputs.as_str().to_string()); + self.no_results = None; + self.pending_search = Some(cx.spawn(|this, mut cx| async move { + let results = search?.await.log_err()?; + let matches = results + .into_iter() + .map(|result| (result.buffer, vec![result.range.start..result.range.start])); + + this.update(&mut cx, |this, cx| { + this.no_results = Some(true); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + }); + }); + for (buffer, ranges) in matches { + let mut match_ranges = this.update(&mut cx, |this, cx| { + this.no_results = Some(false); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) + }) + }); + while let Some(match_range) = match_ranges.next().await { + this.update(&mut cx, |this, cx| { + this.match_ranges.push(match_range); + while let Ok(Some(match_range)) = match_ranges.try_next() { + this.match_ranges.push(match_range); + } + cx.notify(); + }); + } + } + + this.update(&mut cx, |this, cx| { + this.pending_search.take(); + cx.notify(); + }); + + None + })); + cx.notify(); + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ViewEvent { + UpdateTab, + Activate, + EditorEvent(editor::Event), + Dismiss, +} + +impl Entity for ProjectSearchView { + type Event = ViewEvent; +} + +impl View for ProjectSearchView { + fn ui_name() -> &'static str { + "ProjectSearchView" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let model = &self.model.read(cx); + if model.match_ranges.is_empty() { + enum Status {} + + let theme = theme::current(cx).clone(); + + // If Search is Active -> Major: Searching..., Minor: None + // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...} + // If Regex -> Major: "Search using Regex", Minor: {ex...} + // If Text -> Major: "Text search all files and folders", Minor: {...} + + let current_mode = self.current_mode; + let mut major_text = if model.pending_search.is_some() { + Cow::Borrowed("Searching...") + } else if model.no_results.is_some_and(|v| v) { + Cow::Borrowed("No Results") + } else { + match current_mode { + SearchMode::Text => Cow::Borrowed("Text search all files and folders"), + SearchMode::Semantic => { + Cow::Borrowed("Search all code objects using Natural Language") + } + SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), + } + }; + + let mut show_minor_text = true; + let semantic_status = self.semantic_state.as_ref().and_then(|semantic| { + let status = semantic.index_status; + match status { + SemanticIndexStatus::NotAuthenticated => { + major_text = Cow::Borrowed("Not Authenticated"); + show_minor_text = false; + Some(vec![ + "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables." + .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()]) + } + SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]), + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + if remaining_files == 0 { + Some(vec![format!("Indexing...")]) + } else { + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = + rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) { + Some(vec![format!( + "Remaining files to index (rate limit resets in {}s): {}", + remaining_seconds.as_secs(), + remaining_files + )]) + } else { + Some(vec![format!("Remaining files to index: {}", remaining_files)]) + } + } else { + Some(vec![format!("Remaining files to index: {}", remaining_files)]) + } + } + } + SemanticIndexStatus::NotIndexed => None, + } + }); + + let minor_text = if let Some(no_results) = model.no_results { + if model.pending_search.is_none() && no_results { + vec!["No results found in this project for the provided query".to_owned()] + } else { + vec![] + } + } else { + match current_mode { + SearchMode::Semantic => { + let mut minor_text: Vec = Vec::new(); + minor_text.push("".into()); + if let Some(semantic_status) = semantic_status { + minor_text.extend(semantic_status); + } + if show_minor_text { + minor_text + .push("Simply explain the code you are looking to find.".into()); + minor_text.push( + "ex. 'prompt user for permissions to index their project'".into(), + ); + } + minor_text + } + _ => vec![ + "".to_owned(), + "Include/exclude specific paths with the filter option.".to_owned(), + "Matching exact word and/or casing is available too.".to_owned(), + ], + } + }; + + let previous_query_keystrokes = + cx.binding_for_action(&PreviousHistoryQuery {}) + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = + cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + ) + } + (None, Some(next_query_keystrokes)) => { + format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + ) + } + (Some(previous_query_keystrokes), None) => { + format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + ) + } + (None, None) => String::new(), + }; + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(new_placeholder_text, cx); + }); + + MouseEventHandler::new::(0, cx, |_, _| { + Flex::column() + .with_child(Flex::column().contained().flex(1., true)) + .with_child( + Flex::column() + .align_children_center() + .with_child(Label::new( + major_text, + theme.search.major_results_status.clone(), + )) + .with_children( + minor_text.into_iter().map(|x| { + Label::new(x, theme.search.minor_results_status.clone()) + }), + ) + .aligned() + .top() + .contained() + .flex(7., true), + ) + .contained() + .with_background_color(theme.editor.background) + }) + .on_down(MouseButton::Left, |_, _, cx| { + cx.focus_parent(); + }) + .into_any_named("project search view") + } else { + ChildView::new(&self.results_editor, cx) + .flex(1., true) + .into_any_named("project search view") + } + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + let handle = cx.weak_handle(); + cx.update_global(|state: &mut ActiveSearches, cx| { + state + .0 + .insert(self.model.read(cx).project.downgrade(), handle) + }); + + cx.update_global(|state: &mut ActiveSettings, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.current_settings(), + ); + }); + + if cx.is_self_focused() { + if self.query_editor_was_focused { + cx.focus(&self.query_editor); + } else { + cx.focus(&self.results_editor); + } + } + } +} + +impl Item for ProjectSearchView { + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + let query_text = self.query_editor.read(cx).text(cx); + + query_text + .is_empty() + .not() + .then(|| query_text.into()) + .or_else(|| Some("Project Search".into())) + } + fn should_close_item_on_event(event: &Self::Event) -> bool { + event == &Self::Event::Dismiss + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a ViewHandle, + _: &'a AppContext, + ) -> Option<&'a AnyViewHandle> { + if type_id == TypeId::of::() { + Some(self_handle) + } else if type_id == TypeId::of::() { + Some(&self.results_editor) + } else { + None + } + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.deactivated(cx)); + } + + fn tab_content( + &self, + _detail: Option, + tab_theme: &theme::Tab, + cx: &AppContext, + ) -> AnyElement { + Flex::row() + .with_child( + Svg::new("icons/magnifying_glass.svg") + .with_color(tab_theme.label.text.color) + .constrained() + .with_width(tab_theme.type_icon_width) + .aligned() + .contained() + .with_margin_right(tab_theme.spacing), + ) + .with_child({ + let tab_name: Option> = self + .model + .read(cx) + .search_history + .current() + .as_ref() + .map(|query| { + let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); + query_text.into() + }); + Label::new( + tab_name + .filter(|name| !name.is_empty()) + .unwrap_or("Project search".into()), + tab_theme.label.clone(), + ) + .aligned() + }) + .into_any() + } + + fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) { + self.results_editor.for_each_project_item(cx, f) + } + + fn is_singleton(&self, _: &AppContext) -> bool { + false + } + + fn can_save(&self, _: &AppContext) -> bool { + true + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).has_conflict(cx) + } + + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.save(project, cx)) + } + + fn save_as( + &mut self, + _: ModelHandle, + _: PathBuf, + _: &mut ViewContext, + ) -> Task> { + unreachable!("save_as should not have been called") + } + + fn reload( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.reload(project, cx)) + } + + fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext) -> Option + where + Self: Sized, + { + let model = self.model.update(cx, |model, cx| model.clone(cx)); + Some(Self::new(model, cx, None)) + } + + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); + } + + fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + self.results_editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.results_editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + match event { + ViewEvent::UpdateTab => { + smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab] + } + ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event), + ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem], + _ => SmallVec::new(), + } + } + + fn breadcrumb_location(&self) -> ToolbarItemLocation { + if self.has_matches() { + ToolbarItemLocation::Secondary + } else { + ToolbarItemLocation::Hidden + } + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + self.results_editor.breadcrumbs(theme, cx) + } + + fn serialized_item_kind() -> Option<&'static str> { + None + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + _workspace_id: workspace::WorkspaceId, + _item_id: workspace::ItemId, + _cx: &mut ViewContext, + ) -> Task>> { + unimplemented!() + } +} + +impl ProjectSearchView { + fn toggle_filters(&mut self, cx: &mut ViewContext) { + self.filters_enabled = !self.filters_enabled; + cx.update_global(|state: &mut ActiveSettings, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.current_settings(), + ); + }); + } + + fn current_settings(&self) -> ProjectSearchSettings { + ProjectSearchSettings { + search_options: self.search_options, + filters_enabled: self.filters_enabled, + current_mode: self.current_mode, + } + } + fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) { + self.search_options.toggle(option); + cx.update_global(|state: &mut ActiveSettings, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.current_settings(), + ); + }); + } + + fn index_project(&mut self, cx: &mut ViewContext) { + if let Some(semantic_index) = SemanticIndex::global(cx) { + // Semantic search uses no options + self.search_options = SearchOptions::none(); + + let project = self.model.read(cx).project.clone(); + + semantic_index.update(cx, |semantic_index, cx| { + semantic_index + .index_project(project.clone(), cx) + .detach_and_log_err(cx); + }); + + self.semantic_state = Some(SemanticState { + index_status: semantic_index.read(cx).status(&project), + maintain_rate_limit: None, + _subscription: cx.observe(&semantic_index, Self::semantic_index_changed), + }); + self.semantic_index_changed(semantic_index, cx); + } + } + + fn semantic_index_changed( + &mut self, + semantic_index: ModelHandle, + cx: &mut ViewContext, + ) { + let project = self.model.read(cx).project.clone(); + if let Some(semantic_state) = self.semantic_state.as_mut() { + cx.notify(); + semantic_state.index_status = semantic_index.read(cx).status(&project); + if let SemanticIndexStatus::Indexing { + rate_limit_expiry: Some(_), + .. + } = &semantic_state.index_status + { + if semantic_state.maintain_rate_limit.is_none() { + semantic_state.maintain_rate_limit = + Some(cx.spawn(|this, mut cx| async move { + loop { + cx.background().timer(Duration::from_secs(1)).await; + this.update(&mut cx, |_, cx| cx.notify()).log_err(); + } + })); + return; + } + } else { + semantic_state.maintain_rate_limit = None; + } + } + } + + fn clear_search(&mut self, cx: &mut ViewContext) { + self.model.update(cx, |model, cx| { + model.pending_search = None; + model.no_results = None; + model.match_ranges.clear(); + + model.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + }); + }); + } + + fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + let previous_mode = self.current_mode; + if previous_mode == mode { + return; + } + + self.clear_search(cx); + self.current_mode = mode; + self.active_match_index = None; + + match mode { + SearchMode::Semantic => { + let has_permission = self.semantic_permissioned(cx); + self.active_match_index = None; + cx.spawn(|this, mut cx| async move { + let has_permission = has_permission.await?; + + if !has_permission { + let mut answer = this.update(&mut cx, |this, cx| { + let project = this.model.read(cx).project.clone(); + let project_name = project + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join("/"); + let is_plural = + project_name.chars().filter(|letter| *letter == '/').count() > 0; + let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + cx.prompt( + PromptLevel::Info, + prompt_text.as_str(), + &["Continue", "Cancel"], + ) + })?; + + if answer.next().await == Some(0) { + this.update(&mut cx, |this, _| { + this.semantic_permissioned = Some(true); + })?; + } else { + this.update(&mut cx, |this, cx| { + this.semantic_permissioned = Some(false); + debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected"); + this.activate_search_mode(previous_mode, cx); + })?; + return anyhow::Ok(()); + } + } + + this.update(&mut cx, |this, cx| { + this.index_project(cx); + })?; + + anyhow::Ok(()) + }).detach_and_log_err(cx); + } + SearchMode::Regex | SearchMode::Text => { + self.semantic_state = None; + self.active_match_index = None; + self.search(cx); + } + } + + cx.update_global(|state: &mut ActiveSettings, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.current_settings(), + ); + }); + + cx.notify(); + } + fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { + let model = self.model.read(cx); + if let Some(query) = model.active_query.as_ref() { + if model.match_ranges.is_empty() { + return; + } + if let Some(active_index) = self.active_match_index { + let query = query.clone().with_replacement(self.replacement(cx)); + self.results_editor.replace( + &(Box::new(model.match_ranges[active_index].clone()) as _), + &query, + cx, + ); + self.select_match(Direction::Next, cx) + } + } + } + pub fn replacement(&self, cx: &AppContext) -> String { + self.replacement_editor.read(cx).text(cx) + } + fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { + let model = self.model.read(cx); + if let Some(query) = model.active_query.as_ref() { + if model.match_ranges.is_empty() { + return; + } + if self.active_match_index.is_some() { + let query = query.clone().with_replacement(self.replacement(cx)); + let matches = model + .match_ranges + .iter() + .map(|item| Box::new(item.clone()) as _) + .collect::>(); + for item in matches { + self.results_editor.replace(&item, &query, cx); + } + } + } + } + + fn new( + model: ModelHandle, + cx: &mut ViewContext, + settings: Option, + ) -> Self { + let project; + let excerpts; + let mut replacement_text = None; + let mut query_text = String::new(); + + // Read in settings if available + let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings { + ( + settings.search_options, + settings.current_mode, + settings.filters_enabled, + ) + } else { + (SearchOptions::NONE, Default::default(), false) + }; + + { + let model = model.read(cx); + project = model.project.clone(); + excerpts = model.excerpts.clone(); + if let Some(active_query) = model.active_query.as_ref() { + query_text = active_query.as_str().to_string(); + replacement_text = active_query.replacement().map(ToOwned::to_owned); + options = SearchOptions::from_query(active_query); + } + } + cx.observe(&model, |this, _, cx| this.model_changed(cx)) + .detach(); + + let query_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ); + editor.set_placeholder_text("Text search all files", cx); + editor.set_text(query_text, cx); + editor + }); + // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes + cx.subscribe(&query_editor, |_, _, event, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }) + .detach(); + let replacement_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ); + editor.set_placeholder_text("Replace in project..", cx); + if let Some(text) = replacement_text { + editor.set_text(text, cx); + } + editor + }); + let results_editor = cx.add_view(|cx| { + let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx); + editor.set_searchable(false); + editor + }); + cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) + .detach(); + + cx.subscribe(&results_editor, |this, _, event, cx| { + if matches!(event, editor::Event::SelectionsChanged { .. }) { + this.update_match_index(cx); + } + // Reraise editor events for workspace item activation purposes + cx.emit(ViewEvent::EditorEvent(event.clone())); + }) + .detach(); + + let included_files_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| { + theme.search.include_exclude_editor.input.clone() + })), + cx, + ); + editor.set_placeholder_text("Include: crates/**/*.toml", cx); + + editor + }); + // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes + cx.subscribe(&included_files_editor, |_, _, event, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }) + .detach(); + + let excluded_files_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| { + theme.search.include_exclude_editor.input.clone() + })), + cx, + ); + editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx); + + editor + }); + // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes + cx.subscribe(&excluded_files_editor, |_, _, event, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }) + .detach(); + + // Check if Worktrees have all been previously indexed + let mut this = ProjectSearchView { + replacement_editor, + search_id: model.read(cx).search_id, + model, + query_editor, + results_editor, + semantic_state: None, + semantic_permissioned: None, + search_options: options, + panels_with_errors: HashSet::new(), + active_match_index: None, + query_editor_was_focused: false, + included_files_editor, + excluded_files_editor, + filters_enabled, + current_mode, + replace_enabled: false, + }; + this.model_changed(cx); + this + } + + fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { + if let Some(value) = self.semantic_permissioned { + return Task::ready(Ok(value)); + } + + SemanticIndex::global(cx) + .map(|semantic| { + let project = self.model.read(cx).project.clone(); + semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) + }) + .unwrap_or(Task::ready(Ok(false))) + } + pub fn new_search_in_directory( + workspace: &mut Workspace, + dir_entry: &Entry, + cx: &mut ViewContext, + ) { + if !dir_entry.is_dir() { + return; + } + let Some(filter_str) = dir_entry.path.to_str() else { + return; + }; + + let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, None)); + workspace.add_item(Box::new(search.clone()), cx); + search.update(cx, |search, cx| { + search + .included_files_editor + .update(cx, |editor, cx| editor.set_text(filter_str, cx)); + search.filters_enabled = true; + search.focus_query_editor(cx) + }); + } + + // Re-activate the most recently activated search or the most recent if it has been closed. + // If no search exists in the workspace, create a new one. + fn deploy( + workspace: &mut Workspace, + _: &workspace::NewSearch, + cx: &mut ViewContext, + ) { + // Clean up entries for dropped projects + cx.update_global(|state: &mut ActiveSearches, cx| { + state.0.retain(|project, _| project.is_upgradable(cx)) + }); + + let active_search = cx + .global::() + .0 + .get(&workspace.project().downgrade()); + + let existing = active_search + .and_then(|active_search| { + workspace + .items_of_type::(cx) + .find(|search| search == active_search) + }) + .or_else(|| workspace.item_of_type::(cx)); + + let query = workspace.active_item(cx).and_then(|item| { + let editor = item.act_as::(cx)?; + let query = editor.query_suggestion(cx); + if query.is_empty() { + None + } else { + Some(query) + } + }); + + let search = if let Some(existing) = existing { + workspace.activate_item(&existing, cx); + existing + } else { + let settings = cx + .global::() + .0 + .get(&workspace.project().downgrade()); + + let settings = if let Some(settings) = settings { + Some(settings.clone()) + } else { + None + }; + + let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let view = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings)); + + workspace.add_item(Box::new(view.clone()), cx); + view + }; + + search.update(cx, |search, cx| { + if let Some(query) = query { + search.set_query(&query, cx); + } + search.focus_query_editor(cx) + }); + } + + fn search(&mut self, cx: &mut ViewContext) { + let mode = self.current_mode; + match mode { + SearchMode::Semantic => { + if self.semantic_state.is_some() { + if let Some(query) = self.build_search_query(cx) { + self.model + .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx)); + } + } + } + + _ => { + if let Some(query) = self.build_search_query(cx) { + self.model.update(cx, |model, cx| model.search(query, cx)); + } + } + } + } + + fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { + let text = self.query_editor.read(cx).text(cx); + let included_files = + match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) { + Ok(included_files) => { + self.panels_with_errors.remove(&InputPanel::Include); + included_files + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Include); + cx.notify(); + return None; + } + }; + let excluded_files = + match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) { + Ok(excluded_files) => { + self.panels_with_errors.remove(&InputPanel::Exclude); + excluded_files + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Exclude); + cx.notify(); + return None; + } + }; + let current_mode = self.current_mode; + match current_mode { + SearchMode::Regex => { + match SearchQuery::regex( + text, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + included_files, + excluded_files, + ) { + Ok(query) => { + self.panels_with_errors.remove(&InputPanel::Query); + Some(query) + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Query); + cx.notify(); + None + } + } + } + _ => match SearchQuery::text( + text, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + included_files, + excluded_files, + ) { + Ok(query) => { + self.panels_with_errors.remove(&InputPanel::Query); + Some(query) + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Query); + cx.notify(); + None + } + }, + } + } + + fn parse_path_matches(text: &str) -> anyhow::Result> { + text.split(',') + .map(str::trim) + .filter(|maybe_glob_str| !maybe_glob_str.is_empty()) + .map(|maybe_glob_str| { + PathMatcher::new(maybe_glob_str) + .with_context(|| format!("parsing {maybe_glob_str} as path matcher")) + }) + .collect() + } + + fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { + if let Some(index) = self.active_match_index { + let match_ranges = self.model.read(cx).match_ranges.clone(); + let new_index = self.results_editor.update(cx, |editor, cx| { + editor.match_index_for_direction(&match_ranges, index, direction, 1, cx) + }); + + let range_to_select = match_ranges[new_index].clone(); + self.results_editor.update(cx, |editor, cx| { + let range_to_select = editor.range_for_match(&range_to_select); + editor.unfold_ranges([range_to_select.clone()], false, true, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range_to_select]) + }); + }); + } + } + + fn focus_query_editor(&mut self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&SelectAll, cx); + }); + self.query_editor_was_focused = true; + cx.focus(&self.query_editor); + } + + fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + self.query_editor + .update(cx, |query_editor, cx| query_editor.set_text(query, cx)); + } + + fn focus_results_editor(&mut self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + let cursor = query_editor.selections.newest_anchor().head(); + query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor])); + }); + self.query_editor_was_focused = false; + cx.focus(&self.results_editor); + } + + fn model_changed(&mut self, cx: &mut ViewContext) { + let match_ranges = self.model.read(cx).match_ranges.clone(); + if match_ranges.is_empty() { + self.active_match_index = None; + } else { + self.active_match_index = Some(0); + self.update_match_index(cx); + let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); + let is_new_search = self.search_id != prev_search_id; + self.results_editor.update(cx, |editor, cx| { + if is_new_search { + let range_to_select = match_ranges + .first() + .clone() + .map(|range| editor.range_for_match(range)); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(range_to_select) + }); + } + editor.highlight_background::( + match_ranges, + |theme| theme.search.match_background, + cx, + ); + }); + if is_new_search && self.query_editor.is_focused(cx) { + self.focus_results_editor(cx); + } + } + + cx.emit(ViewEvent::UpdateTab); + cx.notify(); + } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + let results_editor = self.results_editor.read(cx); + let new_index = active_match_index( + &self.model.read(cx).match_ranges, + &results_editor.selections.newest_anchor().head(), + &results_editor.buffer().read(cx).snapshot(cx), + ); + if self.active_match_index != new_index { + self.active_match_index = new_index; + cx.notify(); + } + } + + pub fn has_matches(&self) -> bool { + self.active_match_index.is_some() + } + + fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |search_view, cx| { + if !search_view.results_editor.is_focused(cx) + && !search_view.model.read(cx).match_ranges.is_empty() + { + return search_view.focus_results_editor(cx); + } + }); + } + + cx.propagate_action(); + } +} + +impl Default for ProjectSearchBar { + fn default() -> Self { + Self::new() + } +} + +impl ProjectSearchBar { + pub fn new() -> Self { + Self { + active_project_search: Default::default(), + subscription: Default::default(), + } + } + fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |this, cx| { + let new_mode = + crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); + this.activate_search_mode(new_mode, cx); + cx.focus(&this.query_editor); + }) + } + } + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + if !search_view.replacement_editor.is_focused(cx) { + should_propagate = false; + search_view.search(cx); + } + }); + } + if should_propagate { + cx.propagate_action(); + } + } + + fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let new_query = search_view.update(cx, |search_view, cx| { + let new_query = search_view.build_search_query(cx); + if new_query.is_some() { + if let Some(old_query) = search_view.model.read(cx).active_query.clone() { + search_view.query_editor.update(cx, |editor, cx| { + editor.set_text(old_query.as_str(), cx); + }); + search_view.search_options = SearchOptions::from_query(&old_query); + } + } + new_query + }); + if let Some(new_query) = new_query { + let model = cx.add_model(|cx| { + let mut model = ProjectSearch::new(workspace.project().clone(), cx); + model.search(new_query, cx); + model + }); + workspace.add_item( + Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx, None))), + cx, + ); + } + } + } + + fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx)); + } else { + cx.propagate_action(); + } + } + + fn replace_next(pane: &mut Pane, _: &ReplaceNext, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| view.replace_next(&ReplaceNext, cx)); + } else { + cx.propagate_action(); + } + } + fn replace_all(pane: &mut Pane, _: &ReplaceAll, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| view.replace_all(&ReplaceAll, cx)); + } else { + cx.propagate_action(); + } + } + fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx)); + } else { + cx.propagate_action(); + } + } + + fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { + self.cycle_field(Direction::Next, cx); + } + + fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext) { + self.cycle_field(Direction::Prev, cx); + } + + fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext) { + let active_project_search = match &self.active_project_search { + Some(active_project_search) => active_project_search, + + None => { + cx.propagate_action(); + return; + } + }; + + active_project_search.update(cx, |project_view, cx| { + let mut views = vec![&project_view.query_editor]; + if project_view.filters_enabled { + views.extend([ + &project_view.included_files_editor, + &project_view.excluded_files_editor, + ]); + } + if project_view.replace_enabled { + views.push(&project_view.replacement_editor); + } + let current_index = match views + .iter() + .enumerate() + .find(|(_, view)| view.is_focused(cx)) + { + Some((index, _)) => index, + + None => { + cx.propagate_action(); + return; + } + }; + + let new_index = match direction { + Direction::Next => (current_index + 1) % views.len(), + Direction::Prev if current_index == 0 => views.len() - 1, + Direction::Prev => (current_index - 1) % views.len(), + }; + cx.focus(views[new_index]); + }); + } + + fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.toggle_search_option(option, cx); + search_view.search(cx); + }); + + cx.notify(); + true + } else { + false + } + } + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { + if let Some(search) = &self.active_project_search { + search.update(cx, |this, cx| { + this.replace_enabled = !this.replace_enabled; + if !this.replace_enabled { + cx.focus(&this.query_editor); + } + cx.notify(); + }); + } + } + fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |this, cx| { + should_propagate = false; + this.replace_enabled = !this.replace_enabled; + if !this.replace_enabled { + cx.focus(&this.query_editor); + } + cx.notify(); + }); + } + if should_propagate { + cx.propagate_action(); + } + } + fn activate_text_mode(pane: &mut Pane, _: &ActivateTextMode, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| { + view.activate_search_mode(SearchMode::Text, cx) + }); + } else { + cx.propagate_action(); + } + } + + fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| { + view.activate_search_mode(SearchMode::Regex, cx) + }); + } else { + cx.propagate_action(); + } + } + + fn activate_semantic_mode( + pane: &mut Pane, + _: &ActivateSemanticMode, + cx: &mut ViewContext, + ) { + if SemanticIndex::enabled(cx) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| { + view.activate_search_mode(SearchMode::Semantic, cx) + }); + } else { + cx.propagate_action(); + } + } + } + + fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.toggle_filters(cx); + search_view + .included_files_editor + .update(cx, |_, cx| cx.notify()); + search_view + .excluded_files_editor + .update(cx, |_, cx| cx.notify()); + cx.refresh_windows(); + cx.notify(); + }); + cx.notify(); + true + } else { + false + } + } + + fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext) { + // Update Current Mode + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.activate_search_mode(mode, cx); + }); + cx.notify(); + } + } + + fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { + if let Some(search) = self.active_project_search.as_ref() { + search.read(cx).search_options.contains(option) + } else { + false + } + } + + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + let new_query = search_view.model.update(cx, |model, _| { + if let Some(new_query) = model.search_history.next().map(str::to_string) { + new_query + } else { + model.search_history.reset_selection(); + String::new() + } + }); + search_view.set_query(&new_query, cx); + }); + } + } + + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + if search_view.query_editor.read(cx).text(cx).is_empty() { + if let Some(new_query) = search_view + .model + .read(cx) + .search_history + .current() + .map(str::to_string) + { + search_view.set_query(&new_query, cx); + return; + } + } + + if let Some(new_query) = search_view.model.update(cx, |model, _| { + model.search_history.previous().map(str::to_string) + }) { + search_view.set_query(&new_query, cx); + } + }); + } + } +} + +impl Entity for ProjectSearchBar { + type Event = (); +} + +impl View for ProjectSearchBar { + fn ui_name() -> &'static str { + "ProjectSearchBar" + } + + fn update_keymap_context( + &self, + keymap: &mut gpui::keymap_matcher::KeymapContext, + cx: &AppContext, + ) { + Self::reset_to_default_keymap_context(keymap); + let in_replace = self + .active_project_search + .as_ref() + .map(|search| { + search + .read(cx) + .replacement_editor + .read_with(cx, |_, cx| cx.is_self_focused()) + }) + .flatten() + .unwrap_or(false); + if in_replace { + keymap.add_identifier("in_replace"); + } + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(_search) = self.active_project_search.as_ref() { + let search = _search.read(cx); + let theme = theme::current(cx).clone(); + let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) { + theme.search.invalid_editor + } else { + theme.search.editor.input.container + }; + + let search = _search.read(cx); + let filter_button = render_option_button_icon( + search.filters_enabled, + "icons/filter.svg", + 0, + "Toggle filters", + Box::new(ToggleFilters), + move |_, this, cx| { + this.toggle_filters(cx); + }, + cx, + ); + + let search = _search.read(cx); + let is_semantic_available = SemanticIndex::enabled(cx); + let is_semantic_disabled = search.semantic_state.is_none(); + let icon_style = theme.search.editor_icon.clone(); + let is_active = search.active_match_index.is_some(); + + let render_option_button_icon = |path, option, cx: &mut ViewContext| { + crate::search_bar::render_option_button_icon( + self.is_option_enabled(option, cx), + path, + option.bits as usize, + format!("Toggle {}", option.label()), + option.to_toggle_action(), + move |_, this, cx| { + this.toggle_search_option(option, cx); + }, + cx, + ) + }; + let case_sensitive = is_semantic_disabled.then(|| { + render_option_button_icon( + "icons/case_insensitive.svg", + SearchOptions::CASE_SENSITIVE, + cx, + ) + }); + + let whole_word = is_semantic_disabled.then(|| { + render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) + }); + + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + render_search_mode_button( + mode, + side, + is_active, + move |_, this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, + ) + }; + + let search = _search.read(cx); + + let include_container_style = + if search.panels_with_errors.contains(&InputPanel::Include) { + theme.search.invalid_include_exclude_editor + } else { + theme.search.include_exclude_editor.input.container + }; + + let exclude_container_style = + if search.panels_with_errors.contains(&InputPanel::Exclude) { + theme.search.invalid_include_exclude_editor + } else { + theme.search.include_exclude_editor.input.container + }; + + let matches = search.active_match_index.map(|match_ix| { + Label::new( + format!( + "{}/{}", + match_ix + 1, + search.model.read(cx).match_ranges.len() + ), + theme.search.match_index.text.clone(), + ) + .contained() + .with_style(theme.search.match_index.container) + .aligned() + }); + let should_show_replace_input = search.replace_enabled; + let replacement = should_show_replace_input.then(|| { + Flex::row() + .with_child( + Svg::for_style(theme.search.replace_icon.clone().icon) + .contained() + .with_style(theme.search.replace_icon.clone().container), + ) + .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true)) + .align_children_center() + .flex(1., true) + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false) + }); + let replace_all = should_show_replace_input.then(|| { + super::replace_action( + ReplaceAll, + "Replace all", + "icons/replace_all.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let replace_next = should_show_replace_input.then(|| { + super::replace_action( + ReplaceNext, + "Replace next", + "icons/replace_next.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let query_column = Flex::column() + .with_spacing(theme.search.search_row_spacing) + .with_child( + Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container), + ) + .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) + .with_child( + Flex::row() + .with_child(filter_button) + .with_children(case_sensitive) + .with_children(whole_word) + .flex(1., false) + .constrained() + .contained(), + ) + .align_children_center() + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false), + ) + .with_children(search.filters_enabled.then(|| { + Flex::row() + .with_child( + ChildView::new(&search.included_files_editor, cx) + .contained() + .with_style(include_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .with_child( + ChildView::new(&search.excluded_files_editor, cx) + .contained() + .with_style(exclude_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false) + })) + .flex(1., false); + let switches_column = Flex::row() + .align_children_center() + .with_child(super::toggle_replace_button( + search.replace_enabled, + theme.tooltip.clone(), + theme.search.option_button_component.clone(), + )) + .constrained() + .with_height(theme.search.search_bar_row_height) + .contained() + .with_style(theme.search.option_button_group); + let mode_column = + Flex::row() + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + if is_semantic_available { + None + } else { + Some(Side::Right) + }, + cx, + )) + .with_children(is_semantic_available.then(|| { + search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) + })) + .contained() + .with_style(theme.search.modes_container); + + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + render_nav_button( + label, + direction, + is_active, + move |_, this, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |search, cx| search.select_match(direction, cx)); + } + }, + cx, + ) + }; + + let nav_column = Flex::row() + .with_children(replace_next) + .with_children(replace_all) + .with_child(Flex::row().with_children(matches)) + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex_float(); + + Flex::row() + .with_child(query_column) + .with_child(mode_column) + .with_child(switches_column) + .with_children(replacement) + .with_child(nav_column) + .contained() + .with_style(theme.search.container) + .into_any_named("project search") + } else { + Empty::new().into_any() + } + } +} + +impl ToolbarItemView for ProjectSearchBar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + cx.notify(); + self.subscription = None; + self.active_project_search = None; + if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { + search.update(cx, |search, cx| { + if search.current_mode == SearchMode::Semantic { + search.index_project(cx); + } + }); + + self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); + self.active_project_search = Some(search); + ToolbarItemLocation::PrimaryLeft { + flex: Some((1., true)), + } + } else { + ToolbarItemLocation::Hidden + } + } + + fn row_count(&self, cx: &ViewContext) -> usize { + if let Some(search) = self.active_project_search.as_ref() { + if search.read(cx).filters_enabled { + return 2; + } + } + 1 + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use editor::DisplayPoint; + use gpui::{color::Color, executor::Deterministic, TestAppContext}; + use project::FakeFs; + use semantic_index::semantic_index_settings::SemanticIndexSettings; + use serde_json::json; + use settings::SettingsStore; + use std::sync::Arc; + use theme::ThemeSettings; + + #[gpui::test] + async fn test_project_search(deterministic: Arc, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); + let search_view = cx + .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)) + .root(cx); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" + ); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.all_text_background_highlights(cx)), + &[ + ( + DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), + Color::red() + ), + ( + DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40), + Color::red() + ), + ( + DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9), + Color::red() + ) + ] + ); + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + + search_view.select_match(Direction::Next, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + search_view.select_match(Direction::Next, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(Direction::Next, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + search_view.select_match(Direction::Prev, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(Direction::Prev, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + }); + } + + #[gpui::test] + async fn test_project_search_focus(deterministic: Arc, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + + let active_item = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_item.is_none(), + "Expected no search panel to be active, but got: {active_item:?}" + ); + + workspace.update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }); + + let Some(search_view) = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }) else { + panic!("Search view expected to appear after new search event trigger") + }; + let search_view_id = search_view.id(); + + cx.spawn(|mut cx| async move { + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); + }) + .detach(); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.is_focused(cx), + "Empty search view should be focused after the toggle focus event: no results panel to focus on", + ); + }); + + search_view.update(cx, |search_view, cx| { + let query_editor = &search_view.query_editor; + assert!( + query_editor.is_focused(cx), + "Search view should be focused after the new search view is activated", + ); + let query_text = query_editor.read(cx).text(cx); + assert!( + query_text.is_empty(), + "New search query should be empty but got '{query_text}'", + ); + let results_text = search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)); + assert!( + results_text.is_empty(), + "Empty search view should have no results but got '{results_text}'" + ); + }); + + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx) + }); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + let results_text = search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)); + assert!( + results_text.is_empty(), + "Search view for mismatching query should have no results but got '{results_text}'" + ); + assert!( + search_view.query_editor.is_focused(cx), + "Search view should be focused after mismatching query had been used in search", + ); + }); + cx.spawn( + |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) }, + ) + .detach(); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.is_focused(cx), + "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", + ); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "Search view results should match the query" + ); + assert!( + search_view.results_editor.is_focused(cx), + "Search view with mismatching query should be focused after search results are available", + ); + }); + cx.spawn(|mut cx| async move { + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); + }) + .detach(); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.results_editor.is_focused(cx), + "Search view with matching query should still have its results editor focused after the toggle focus event", + ); + }); + + workspace.update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "Results should be unchanged after search view 2nd open in a row" + ); + assert!( + search_view.query_editor.is_focused(cx), + "Focus should be moved into query editor again after search view 2nd open in a row" + ); + }); + + cx.spawn(|mut cx| async move { + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); + }) + .detach(); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.results_editor.is_focused(cx), + "Search view with matching query should switch focus to the results editor after the toggle focus event", + ); + }); + } + + #[gpui::test] + async fn test_new_project_search_in_directory( + deterministic: Arc, + cx: &mut TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a": { + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + }, + "b": { + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let worktree_id = project.read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .root(cx); + + let active_item = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_item.is_none(), + "Expected no search panel to be active, but got: {active_item:?}" + ); + + let one_file_entry = cx.update(|cx| { + workspace + .read(cx) + .project() + .read(cx) + .entry_for_path(&(worktree_id, "a/one.rs").into(), cx) + .expect("no entry for /a/one.rs file") + }); + assert!(one_file_entry.is_file()); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx) + }); + let active_search_entry = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_search_entry.is_none(), + "Expected no search panel to be active for file entry" + ); + + let a_dir_entry = cx.update(|cx| { + workspace + .read(cx) + .project() + .read(cx) + .entry_for_path(&(worktree_id, "a").into(), cx) + .expect("no entry for /a/ directory") + }); + assert!(a_dir_entry.is_dir()); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx) + }); + + let Some(search_view) = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }) else { + panic!("Search view expected to appear after new search in directory event trigger") + }; + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.is_focused(cx), + "On new search in directory, focus should be moved into query editor" + ); + search_view.excluded_files_editor.update(cx, |editor, cx| { + assert!( + editor.display_text(cx).is_empty(), + "New search in directory should not have any excluded files" + ); + }); + search_view.included_files_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + a_dir_entry.path.to_str().unwrap(), + "New search in directory should have included dir entry path" + ); + }); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("const", cx)); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "New search in directory should have a filter that matches a certain directory" + ); + }); + } + + #[gpui::test] + async fn test_search_query_history(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }); + + let search_view = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .expect("Search view expected to appear after new search event trigger") + }); + + let search_bar = window.add_view(cx, |cx| { + let mut search_bar = ProjectSearchBar::new(); + search_bar.set_active_pane_item(Some(&search_view), cx); + // search_bar.show(cx); + search_bar + }); + + // Add 3 search items into the history + another unsubmitted one. + search_view.update(cx, |search_view, cx| { + search_view.search_options = SearchOptions::CASE_SENSITIVE; + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("JUST_TEXT_INPUT", cx) + }); + }); + cx.foreground().run_until_parked(); + + // Ensure that the latest input with search settings is active. + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view.query_editor.read(cx).text(cx), + "JUST_TEXT_INPUT" + ); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next history query after the latest should set the query to the empty string. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // First previous query for empty current query should set the query to the latest submitted one. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Further previous items should go over the history in reverse order. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Previous items should never go behind the first history item. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next items should go over the history in the original order. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // New search input should add another entry to history and move the selection to the end of the history. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + } + + pub fn init_test(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); + theme.search.match_background = Color::red(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + cx.set_global(ActiveSearches::default()); + settings::register::(cx); + + theme::init((), cx); + cx.update_global::(|store, _| { + let mut settings = store.get::(None).clone(); + settings.theme = Arc::new(theme); + store.override_global(settings) + }); + + language::init(cx); + client::init_settings(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + super::init(cx); + }); + } +} diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs new file mode 100644 index 0000000000..9ae66462fc --- /dev/null +++ b/crates/search2/src/search.rs @@ -0,0 +1,115 @@ +use bitflags::bitflags; +pub use buffer_search::BufferSearchBar; +use gpui::{actions, Action, AnyElement, AppContext, Component, Element, Svg, View}; +pub use mode::SearchMode; +use project::search::SearchQuery; +use ui::ButtonVariant; +//pub use project_search::{ProjectSearchBar, ProjectSearchView}; +// use theme::components::{ +// action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, +// }; + +pub mod buffer_search; +mod history; +mod mode; +//pub mod project_search; +pub(crate) mod search_bar; + +pub fn init(cx: &mut AppContext) { + buffer_search::init(cx); + //project_search::init(cx); +} + +actions!( + CycleMode, + ToggleWholeWord, + ToggleCaseSensitive, + ToggleReplace, + SelectNextMatch, + SelectPrevMatch, + SelectAllMatches, + NextHistoryQuery, + PreviousHistoryQuery, + ActivateTextMode, + ActivateSemanticMode, + ActivateRegexMode, + ReplaceAll, + ReplaceNext, +); + +bitflags! { + #[derive(Default)] + pub struct SearchOptions: u8 { + const NONE = 0b000; + const WHOLE_WORD = 0b001; + const CASE_SENSITIVE = 0b010; + } +} + +impl SearchOptions { + pub fn label(&self) -> &'static str { + match *self { + SearchOptions::WHOLE_WORD => "Match Whole Word", + SearchOptions::CASE_SENSITIVE => "Match Case", + _ => panic!("{:?} is not a named SearchOption", self), + } + } + + pub fn icon(&self) -> ui::Icon { + match *self { + SearchOptions::WHOLE_WORD => ui::Icon::WholeWord, + SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive, + _ => panic!("{:?} is not a named SearchOption", self), + } + } + + pub fn to_toggle_action(&self) -> Box { + match *self { + SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), + SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), + _ => panic!("{:?} is not a named SearchOption", self), + } + } + + pub fn none() -> SearchOptions { + SearchOptions::NONE + } + + pub fn from_query(query: &SearchQuery) -> SearchOptions { + let mut options = SearchOptions::NONE; + options.set(SearchOptions::WHOLE_WORD, query.whole_word()); + options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); + options + } + + pub fn as_button(&self, active: bool) -> impl Component { + ui::IconButton::new(0, self.icon()) + .on_click({ + let action = self.to_toggle_action(); + move |_: &mut V, cx| { + cx.dispatch_action(action.boxed_clone()); + } + }) + .variant(ui::ButtonVariant::Ghost) + .when(active, |button| button.variant(ButtonVariant::Filled)) + } +} + +fn toggle_replace_button(active: bool) -> impl Component { + // todo: add toggle_replace button + ui::IconButton::new(0, ui::Icon::Replace) + .on_click(|_: &mut V, cx| { + cx.dispatch_action(Box::new(ToggleReplace)); + }) + .variant(ui::ButtonVariant::Ghost) + .when(active, |button| button.variant(ButtonVariant::Filled)) +} + +fn replace_action( + action: impl Action + 'static + Send + Sync, + name: &'static str, +) -> impl Component { + ui::IconButton::new(0, ui::Icon::Replace).on_click(move |_: &mut V, cx| { + cx.dispatch_action(action.boxed_clone()); + }) +} diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs new file mode 100644 index 0000000000..150b2f2d89 --- /dev/null +++ b/crates/search2/src/search_bar.rs @@ -0,0 +1,177 @@ +use std::borrow::Cow; + +use gpui::{ + div, Action, AnyElement, Component, CursorStyle, Element, MouseButton, MouseDownEvent, Svg, + View, ViewContext, +}; +use theme::ActiveTheme; +use ui::Label; +use workspace::searchable::Direction; + +use crate::{ + mode::{SearchMode, Side}, + SelectNextMatch, SelectPrevMatch, +}; + +pub(super) fn render_nav_button( + icon: &'static str, + direction: Direction, + active: bool, + on_click: impl Fn(MouseDownEvent, &mut V, &mut ViewContext) + 'static, + cx: &mut ViewContext, +) -> impl Component { + let action: Box; + let tooltip; + + match direction { + Direction::Prev => { + action = Box::new(SelectPrevMatch); + tooltip = "Select Previous Match"; + } + Direction::Next => { + action = Box::new(SelectNextMatch); + tooltip = "Select Next Match"; + } + }; + // let tooltip_style = cx.theme().tooltip.clone(); + // let cursor_style = if active { + // CursorStyle::PointingHand + // } else { + // CursorStyle::default() + // }; + // enum NavButton {} + div() + // MouseEventHandler::new::(direction as usize, cx, |state, cx| { + // let theme = cx.theme(); + // let style = theme + // .search + // .nav_button + // .in_state(active) + // .style_for(state) + // .clone(); + // let mut container_style = style.container.clone(); + // let label = Label::new(icon, style.label.clone()).aligned().contained(); + // container_style.corner_radii = match direction { + // Direction::Prev => CornerRadii { + // bottom_right: 0., + // top_right: 0., + // ..container_style.corner_radii + // }, + // Direction::Next => CornerRadii { + // bottom_left: 0., + // top_left: 0., + // ..container_style.corner_radii + // }, + // }; + // if direction == Direction::Prev { + // // Remove right border so that when both Next and Prev buttons are + // // next to one another, there's no double border between them. + // container_style.border.right = false; + // } + // label.with_style(container_style) + // }) + // .on_click(MouseButton::Left, on_click) + // .with_cursor_style(cursor_style) + // .with_tooltip::( + // direction as usize, + // tooltip.to_string(), + // Some(action), + // tooltip_style, + // cx, + // ) + // .into_any() +} + +pub(crate) fn render_search_mode_button( + mode: SearchMode, + side: Option, + is_active: bool, + //on_click: impl Fn(MouseClick, &mut V, &mut ViewContext) + 'static, + cx: &mut ViewContext, +) -> impl Component { + //let tooltip_style = cx.theme().tooltip.clone(); + enum SearchModeButton {} + div() + // MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { + // let theme = cx.theme(); + // let style = theme + // .search + // .mode_button + // .in_state(is_active) + // .style_for(state) + // .clone(); + + // let mut container_style = style.container; + // if let Some(button_side) = side { + // if button_side == Side::Left { + // container_style.border.left = true; + // container_style.corner_radii = CornerRadii { + // bottom_right: 0., + // top_right: 0., + // ..container_style.corner_radii + // }; + // } else { + // container_style.border.left = false; + // container_style.corner_radii = CornerRadii { + // bottom_left: 0., + // top_left: 0., + // ..container_style.corner_radii + // }; + // } + // } else { + // container_style.border.left = false; + // container_style.corner_radii = CornerRadii::default(); + // } + + // Label::new(mode.label(), style.text) + // .aligned() + // .contained() + // .with_style(container_style) + // .constrained() + // .with_height(theme.search.search_bar_row_height) + // }) + // .on_click(MouseButton::Left, on_click) + // .with_cursor_style(CursorStyle::PointingHand) + // .with_tooltip::( + // mode.region_id(), + // mode.tooltip_text().to_owned(), + // Some(mode.activate_action()), + // tooltip_style, + // cx, + // ) + // .into_any() +} + +pub(crate) fn render_option_button_icon( + is_active: bool, + icon: &'static str, + id: usize, + label: impl Into>, + action: Box, + //on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, + cx: &mut ViewContext, +) -> impl Component { + //let tooltip_style = cx.theme().tooltip.clone(); + div() + // MouseEventHandler::new::(id, cx, |state, cx| { + // let theme = cx.theme(); + // let style = theme + // .search + // .option_button + // .in_state(is_active) + // .style_for(state); + // Svg::new(icon) + // .with_color(style.color.clone()) + // .constrained() + // .with_width(style.icon_width) + // .contained() + // .with_style(style.container) + // .constrained() + // .with_height(theme.search.option_button_height) + // .with_width(style.button_width) + // }) + // .on_click(MouseButton::Left, on_click) + // .with_cursor_style(CursorStyle::PointingHand) + // .with_tooltip::(id, label, Some(action), tooltip_style, cx) + // .into_any() +} diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 907f3f9187..28c3c42e51 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -97,6 +97,8 @@ pub enum Icon { BellRing, MailOpen, AtSign, + WholeWord, + CaseSensitive, } impl Icon { @@ -155,6 +157,8 @@ impl Icon { Icon::BellRing => "icons/bell-ring.svg", Icon::MailOpen => "icons/mail-open.svg", Icon::AtSign => "icons/at-sign.svg", + Icon::WholeWord => "icons/word_search.svg", + Icon::CaseSensitive => "icons/case_insensitive.svg", } } } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2bba684d12..4698c15d57 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1909,7 +1909,7 @@ impl Render for Pane { v_stack() .size_full() .child(self.render_tab_bar(cx)) - .child(div() /* todo!(toolbar) */) + .child(self.toolbar.clone()) .child(if let Some(item) = self.active_item() { div().flex_1().child(item.to_any()) } else { diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs index 2a393a9f6d..ef3a5f08fc 100644 --- a/crates/workspace2/src/searchable.rs +++ b/crates/workspace2/src/searchable.rs @@ -1,7 +1,8 @@ use std::{any::Any, sync::Arc}; use gpui::{ - AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WindowContext, + AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView, + WindowContext, }; use project2::search::SearchQuery; @@ -129,8 +130,7 @@ pub trait SearchableItemHandle: ItemHandle { // todo!("here is where we need to use AnyWeakView"); impl SearchableItemHandle for View { fn downgrade(&self) -> Box { - // Box::new(self.downgrade()) - todo!() + Box::new(self.downgrade()) } fn boxed_clone(&self) -> Box { @@ -252,16 +252,15 @@ pub trait WeakSearchableItemHandle: WeakItemHandle { // fn into_any(self) -> AnyWeakView; } -// todo!() -// impl WeakSearchableItemHandle for WeakView { -// fn upgrade(&self, cx: &AppContext) -> Option> { -// Some(Box::new(self.upgrade(cx)?)) -// } +impl WeakSearchableItemHandle for WeakView { + fn upgrade(&self, cx: &AppContext) -> Option> { + Some(Box::new(self.upgrade()?)) + } -// // fn into_any(self) -> AnyView { -// // self.into_any() -// // } -// } + // fn into_any(self) -> AnyView { + // self.into_any() + // } +} impl PartialEq for Box { fn eq(&self, other: &Self) -> bool { diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 1d67da06b2..ddbb801890 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,6 +1,7 @@ use crate::ItemHandle; use gpui::{ - AnyView, Div, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext, + div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement, Render, View, ViewContext, + WindowContext, }; pub enum ToolbarItemEvent { @@ -55,7 +56,8 @@ impl Render for Toolbar { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() + //dbg!(&self.items.len()); + div().children(self.items.iter().map(|(child, _)| child.to_any())) } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index e4fc5d35c6..beb4331f1e 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -68,7 +68,7 @@ use std::{ time::Duration, }; use theme2::ActiveTheme; -pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; +pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; use ui::{h_stack, Label}; use util::ResultExt; use uuid::Uuid; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 570912abc5..9eaa80fab1 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -37,7 +37,7 @@ db = { package = "db2", path = "../db2" } editor = { package="editor2", path = "../editor2" } # feedback = { path = "../feedback" } # file_finder = { path = "../file_finder" } -# search = { path = "../search" } +search = { package = "search2", path = "../search2" } fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index c9e7ee8c58..895f7fe3cf 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -194,7 +194,7 @@ fn main() { // project_panel::init(Assets, cx); // channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); - // search::init(cx); + search::init(cx); // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); // vim::init(cx); // terminal_view::init(cx); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 7368d3a5ef..1c73076ba5 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -8,8 +8,8 @@ mod open_listener; pub use assets::*; use gpui::{ - point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, WeakView, WindowBounds, - WindowKind, WindowOptions, + point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, VisualContext as _, WeakView, + WindowBounds, WindowKind, WindowOptions, }; pub use only_instance::*; pub use open_listener::*; @@ -64,8 +64,8 @@ pub fn initialize_workspace( // todo!() // let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); // toolbar.add_item(breadcrumbs, cx); - // let buffer_search_bar = cx.add_view(BufferSearchBar::new); - // toolbar.add_item(buffer_search_bar.clone(), cx); + let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); + toolbar.add_item(buffer_search_bar.clone(), cx); // let quick_action_bar = cx.add_view(|_| { // QuickActionBar::new(buffer_search_bar, workspace) // }); From 6c69e40e5c5cfe77b50521ed733bdf42f015e280 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:56:31 +0100 Subject: [PATCH 002/205] WIP --- crates/editor2/src/items.rs | 1 - crates/search2/src/buffer_search.rs | 218 +++++++++++++++++----------- crates/search2/src/search.rs | 1 + crates/search2/src/search_bar.rs | 28 ++-- crates/workspace2/src/toolbar.rs | 4 +- 5 files changed, 158 insertions(+), 94 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 6b396278b6..f385fd151c 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -910,7 +910,6 @@ impl SearchableItem for Editor { } fn update_matches(&mut self, matches: Vec>, cx: &mut ViewContext) { - dbg!(&matches); self.highlight_background::( matches, |theme| theme.title_bar_background, // todo: update theme diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index fdd03f71c3..297daebe58 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -10,16 +10,16 @@ use collections::HashMap; use editor::Editor; use futures::channel::oneshot; use gpui::{ - action, actions, div, Action, AnyElement, AnyView, AppContext, Component, Div, Entity, - EventEmitter, ParentElement as _, Render, Subscription, Svg, Task, View, ViewContext, - VisualContext as _, WindowContext, + action, actions, blue, div, red, white, Action, AnyElement, AnyView, AppContext, Component, + Div, Entity, EventEmitter, Hsla, ParentElement as _, Render, Styled, Subscription, Svg, Task, + View, ViewContext, VisualContext as _, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; use std::{any::Any, sync::Arc}; use theme::ActiveTheme; -use ui::{IconButton, Label}; +use ui::{h_stack, Icon, IconButton, IconElement, Label, StyledExt}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -66,31 +66,8 @@ pub struct BufferSearchBar { impl EventEmitter for BufferSearchBar {} impl EventEmitter for BufferSearchBar {} impl Render for BufferSearchBar { - // fn ui_name() -> &'static str { - // "BufferSearchBar" - // } - - // fn update_keymap_context( - // &self, - // keymap: &mut gpui::keymap_matcher::KeymapContext, - // cx: &AppContext, - // ) { - // Self::reset_to_default_keymap_context(keymap); - // let in_replace = self - // .replacement_editor - // .read_with(cx, |_, cx| cx.is_self_focused()) - // .unwrap_or(false); - // if in_replace { - // keymap.add_identifier("in_replace"); - // } - // } - - // fn focus_in(&mut self, _: View, cx: &mut ViewContext) { - // cx.focus(&self.query_editor); - // } type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let theme = cx.theme().clone(); // let query_container_style = if self.query_contains_error { // theme.search.invalid_editor // } else { @@ -147,61 +124,104 @@ impl Render for BufferSearchBar { self.replacement_editor.update(cx, |editor, cx| { editor.set_placeholder_text("Replace with...", cx); }); + + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + let is_active = self.current_mode == mode; + + render_search_mode_button( + mode, + side, + is_active, + move |this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, + ) + }; + let search_option_button = |option| { + let is_active = self.search_options.contains(option); + option.as_button(is_active) + }; + let match_count = self + .active_searchable_item + .as_ref() + .and_then(|searchable_item| { + if self.query(cx).is_empty() { + return None; + } + let matches = self + .searchable_items_with_matches + .get(&searchable_item.downgrade())?; + let message = if let Some(match_ix) = self.active_match_index { + format!("{}/{}", match_ix + 1, matches.len()) + } else { + "No matches".to_string() + }; + + Some(ui::Label::new(message)) + }); + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + render_nav_button( + label, + direction, + self.active_match_index.is_some(), + move |this, cx| match direction { + Direction::Prev => this.select_prev_match(&Default::default(), cx), + Direction::Next => this.select_next_match(&Default::default(), cx), + }, + cx, + ) + }; div() - .child(self.query_editor.clone()) - .child(self.replacement_editor.clone()) - // let search_button_for_mode = |mode, side, cx: &mut ViewContext| { - // let is_active = self.current_mode == mode; + .w_full() + .border() + .border_color(blue()) + .flex() // Make this div a flex container + .justify_between() + .child( + div() + .flex() + .border_1() + .border_color(red()) + .rounded_md() + .w_96() + .items_center() + .child(IconElement::new(Icon::MagnifyingGlass)) + .child(self.query_editor.clone()) + .children( + supported_options + .case + .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)), + ) + .children( + supported_options + .word + .then(|| search_option_button(SearchOptions::WHOLE_WORD)), + ), + ) + .child(div().w_auto().flex_row()) + .child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .child(search_button_for_mode( + SearchMode::Regex, + Some(Side::Right), + cx, + )) + .when(supported_options.replacement, |this| { + this.child(super::toggle_replace_button(self.replace_enabled)) + }) + .when(self.replace_enabled, |this| { + this.child(div().w_80().child(self.replacement_editor.clone())) + }) + .children(match_count) + .child(nav_button_for_direction("<", Direction::Prev, cx)) + .child(nav_button_for_direction(">", Direction::Next, cx)) + .flex() + .justify_between() - // render_search_mode_button( - // mode, - // side, - // is_active, - // move |_, this, cx| { - // this.activate_search_mode(mode, cx); - // }, - // cx, - // ) - // }; - // let search_option_button = |option| { - // let is_active = self.search_options.contains(option); - // option.as_button(is_active) - // }; - // let match_count = self - // .active_searchable_item - // .as_ref() - // .and_then(|searchable_item| { - // if self.query(cx).is_empty() { - // return None; - // } - // let matches = self - // .searchable_items_with_matches - // .get(&searchable_item.downgrade())?; - // let message = if let Some(match_ix) = self.active_match_index { - // format!("{}/{}", match_ix + 1, matches.len()) - // } else { - // "No matches".to_string() - // }; - - // Some( - // Label::new(message) - // .contained() - // .with_style(theme.search.match_index.container) - // .aligned(), - // ) - // }); - // let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { - // render_nav_button( - // label, - // direction, - // self.active_match_index.is_some(), - // move |_, this, cx| match direction { - // Direction::Prev => this.select_prev_match(&Default::default(), cx), - // Direction::Next => this.select_next_match(&Default::default(), cx), - // }, - // cx, - // ) - // }; // let query_column = Flex::row() // .with_child( // Svg::for_style(theme.search.editor_icon.clone().icon) @@ -351,7 +371,6 @@ impl ToolbarItemView for BufferSearchBar { impl BufferSearchBar { pub fn register(workspace: &mut Workspace) { workspace.register_action(|workspace, a: &Deploy, cx| { - dbg!("Setting"); workspace.active_pane().update(cx, |this, cx| { this.toolbar().update(cx, |this, cx| { let view = cx.build_view(|cx| BufferSearchBar::new(cx)); @@ -361,6 +380,43 @@ impl BufferSearchBar { }) }); }); + fn register_action( + workspace: &mut Workspace, + update: fn(&mut BufferSearchBar, &mut ViewContext<'_, BufferSearchBar>), + ) { + workspace.register_action(move |workspace, _: &A, cx| { + workspace.active_pane().update(cx, move |this, cx| { + this.toolbar().update(cx, move |toolbar, cx| { + let Some(search_bar) = toolbar.item_of_type::() else { + return; + }; + search_bar.update(cx, |this, cx| update(this, cx)) + }) + }); + }); + } + register_action::(workspace, |this, cx| { + this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) + }); + register_action::(workspace, |this: &mut BufferSearchBar, cx| { + this.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + }); + register_action::(workspace, |this: &mut BufferSearchBar, cx| { + dbg!("Toggling"); + this.toggle_replace(&ToggleReplace, cx) + }); + // workspace.register_action(|workspace, _: &ToggleCaseSensitive, cx| { + // workspace.active_pane().update(cx, |this, cx| { + // this.toolbar().update(cx, |toolbar, cx| { + // let Some(search_bar) = toolbar.item_of_type::() else { + // return; + // }; + // search_bar.update(cx, |this, cx| { + // this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + // }) + // }) + // }); + // }); } pub fn new(cx: &mut ViewContext) -> Self { dbg!("New"); diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 9ae66462fc..955328c483 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -100,6 +100,7 @@ fn toggle_replace_button(active: bool) -> impl Component { ui::IconButton::new(0, ui::Icon::Replace) .on_click(|_: &mut V, cx| { cx.dispatch_action(Box::new(ToggleReplace)); + cx.notify(); }) .variant(ui::ButtonVariant::Ghost) .when(active, |button| button.variant(ButtonVariant::Filled)) diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index 150b2f2d89..d9259edd3f 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -1,11 +1,11 @@ -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; use gpui::{ - div, Action, AnyElement, Component, CursorStyle, Element, MouseButton, MouseDownEvent, Svg, - View, ViewContext, + div, Action, AnyElement, Component, CursorStyle, Element, MouseButton, MouseDownEvent, + ParentElement as _, StatelessInteractive, Styled, Svg, View, ViewContext, }; use theme::ActiveTheme; -use ui::Label; +use ui::{Button, Label}; use workspace::searchable::Direction; use crate::{ @@ -17,19 +17,16 @@ pub(super) fn render_nav_button( icon: &'static str, direction: Direction, active: bool, - on_click: impl Fn(MouseDownEvent, &mut V, &mut ViewContext) + 'static, + on_click: impl Fn(&mut V, &mut ViewContext) + 'static + Send + Sync, cx: &mut ViewContext, ) -> impl Component { - let action: Box; let tooltip; match direction { Direction::Prev => { - action = Box::new(SelectPrevMatch); tooltip = "Select Previous Match"; } Direction::Next => { - action = Box::new(SelectNextMatch); tooltip = "Select Next Match"; } }; @@ -40,7 +37,7 @@ pub(super) fn render_nav_button( // CursorStyle::default() // }; // enum NavButton {} - div() + Button::new(icon).on_click(Arc::new(on_click)) // MouseEventHandler::new::(direction as usize, cx, |state, cx| { // let theme = cx.theme(); // let style = theme @@ -86,12 +83,23 @@ pub(crate) fn render_search_mode_button( mode: SearchMode, side: Option, is_active: bool, - //on_click: impl Fn(MouseClick, &mut V, &mut ViewContext) + 'static, + on_click: impl Fn(&mut V, &mut ViewContext) + 'static, cx: &mut ViewContext, ) -> impl Component { //let tooltip_style = cx.theme().tooltip.clone(); enum SearchModeButton {} + div() + .border_2() + .rounded_md() + .when(side == Some(Side::Left), |this| { + this.border_r_0().rounded_tr_none().rounded_br_none() + }) + .when(side == Some(Side::Right), |this| { + this.border_l_0().rounded_bl_none().rounded_tl_none() + }) + .on_key_down(move |v, _, _, cx| on_click(v, cx)) + .child(Label::new(mode.label())) // MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { // let theme = cx.theme(); // let style = theme diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index ddbb801890..6ad03d8a84 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,7 +1,7 @@ use crate::ItemHandle; use gpui::{ - div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement, Render, View, ViewContext, - WindowContext, + div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement, Render, Styled, View, + ViewContext, WindowContext, }; pub enum ToolbarItemEvent { From 08dde94299a8f90fb8109bd4787ed526c0146f28 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 14 Nov 2023 17:59:53 +0100 Subject: [PATCH 003/205] WIP styling Co-authored-by: Nate --- crates/editor2/src/items.rs | 2 +- crates/search2/src/buffer_search.rs | 29 +++++++--------- crates/search2/src/search_bar.rs | 38 +++++++++++++-------- crates/workspace2/src/toolbar.rs | 53 ++++++++++++++++++++++++++--- 4 files changed, 85 insertions(+), 37 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index f385fd151c..90b821f99d 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -760,7 +760,7 @@ impl Item for Editor { } fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft { flex: None } + ToolbarItemLocation::PrimaryLeft } fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option> { diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 297daebe58..72c9390511 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -10,16 +10,16 @@ use collections::HashMap; use editor::Editor; use futures::channel::oneshot; use gpui::{ - action, actions, blue, div, red, white, Action, AnyElement, AnyView, AppContext, Component, - Div, Entity, EventEmitter, Hsla, ParentElement as _, Render, Styled, Subscription, Svg, Task, - View, ViewContext, VisualContext as _, WindowContext, + action, actions, blue, div, red, rems, white, Action, AnyElement, AnyView, AppContext, + Component, Div, Entity, EventEmitter, Hsla, ParentElement as _, Render, Styled, Subscription, + Svg, Task, View, ViewContext, VisualContext as _, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; use std::{any::Any, sync::Arc}; use theme::ActiveTheme; -use ui::{h_stack, Icon, IconButton, IconElement, Label, StyledExt}; +use ui::{h_stack, Button, ButtonGroup, Icon, IconButton, IconElement, Label, StyledExt}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -173,10 +173,9 @@ impl Render for BufferSearchBar { ) }; div() - .w_full() .border() .border_color(blue()) - .flex() // Make this div a flex container + .flex() .justify_between() .child( div() @@ -199,17 +198,10 @@ impl Render for BufferSearchBar { .then(|| search_option_button(SearchOptions::WHOLE_WORD)), ), ) - .child(div().w_auto().flex_row()) - .child(search_button_for_mode( - SearchMode::Text, - Some(Side::Left), - cx, - )) - .child(search_button_for_mode( - SearchMode::Regex, - Some(Side::Right), - cx, - )) + .child(ButtonGroup::new(vec![ + search_button_for_mode(SearchMode::Text, Some(Side::Left), cx), + search_button_for_mode(SearchMode::Regex, Some(Side::Right), cx), + ])) .when(supported_options.replacement, |this| { this.child(super::toggle_replace_button(self.replace_enabled)) }) @@ -373,6 +365,9 @@ impl BufferSearchBar { workspace.register_action(|workspace, a: &Deploy, cx| { workspace.active_pane().update(cx, |this, cx| { this.toolbar().update(cx, |this, cx| { + if this.item_of_type::().is_some() { + return; + } let view = cx.build_view(|cx| BufferSearchBar::new(cx)); this.add_item(view.clone(), cx); view.update(cx, |this, cx| this.deploy(a, cx)); diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index d9259edd3f..8c3dc2e698 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -5,7 +5,7 @@ use gpui::{ ParentElement as _, StatelessInteractive, Styled, Svg, View, ViewContext, }; use theme::ActiveTheme; -use ui::{Button, Label}; +use ui::{v_stack, Button, ButtonVariant, Label}; use workspace::searchable::Direction; use crate::{ @@ -83,23 +83,33 @@ pub(crate) fn render_search_mode_button( mode: SearchMode, side: Option, is_active: bool, - on_click: impl Fn(&mut V, &mut ViewContext) + 'static, + on_click: impl Fn(&mut V, &mut ViewContext) + 'static + Send + Sync, cx: &mut ViewContext, -) -> impl Component { +) -> Button { //let tooltip_style = cx.theme().tooltip.clone(); enum SearchModeButton {} - div() - .border_2() - .rounded_md() - .when(side == Some(Side::Left), |this| { - this.border_r_0().rounded_tr_none().rounded_br_none() - }) - .when(side == Some(Side::Right), |this| { - this.border_l_0().rounded_bl_none().rounded_tl_none() - }) - .on_key_down(move |v, _, _, cx| on_click(v, cx)) - .child(Label::new(mode.label())) + let button_variant = if is_active { + ButtonVariant::Filled + } else { + ButtonVariant::Ghost + }; + + Button::new(mode.label()) + .on_click(Arc::new(on_click)) + .variant(button_variant) + + // v_stack() + // .border_2() + // .rounded_md() + // .when(side == Some(Side::Left), |this| { + // this.border_r_0().rounded_tr_none().rounded_br_none() + // }) + // .when(side == Some(Side::Right), |this| { + // this.border_l_0().rounded_bl_none().rounded_tl_none() + // }) + // .on_key_down(move |v, _, _, cx| on_click(v, cx)) + // .child(Label::new(mode.label())) // MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { // let theme = cx.theme(); // let style = theme diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 6ad03d8a84..e03e533314 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,8 +1,9 @@ use crate::ItemHandle; use gpui::{ - div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement, Render, Styled, View, - ViewContext, WindowContext, + div, AnyView, Component, Div, Entity, EntityId, EventEmitter, ParentElement, Render, Styled, + View, ViewContext, WindowContext, }; +use ui::{h_stack, v_stack, Button, Icon, IconButton, Label, LabelColor, StyledExt}; pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), @@ -40,8 +41,8 @@ trait ToolbarItemViewHandle: Send { #[derive(Copy, Clone, Debug, PartialEq)] pub enum ToolbarItemLocation { Hidden, - PrimaryLeft { flex: Option<(f32, bool)> }, - PrimaryRight { flex: Option<(f32, bool)> }, + PrimaryLeft, + PrimaryRight, Secondary, } @@ -52,12 +53,54 @@ pub struct Toolbar { items: Vec<(Box, ToolbarItemLocation)>, } +impl Toolbar { + fn left_items(&self) -> impl Iterator { + self.items.iter().filter_map(|(item, location)| { + if *location == ToolbarItemLocation::PrimaryLeft { + Some(item.as_ref()) + } else { + None + } + }) + } + + fn right_items(&self) -> impl Iterator { + self.items.iter().filter_map(|(item, location)| { + if *location == ToolbarItemLocation::PrimaryRight { + Some(item.as_ref()) + } else { + None + } + }) + } +} + impl Render for Toolbar { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { //dbg!(&self.items.len()); - div().children(self.items.iter().map(|(child, _)| child.to_any())) + v_stack() + .child( + h_stack() + .justify_between() + .child( + // Toolbar left side + h_stack() + .p_1() + .child(Button::new("crates")) + .child(Label::new("/").color(LabelColor::Muted)) + .child(Button::new("workspace2")), + ) + // Toolbar right side + .child( + h_stack() + .p_1() + .child(IconButton::new("buffer-search", Icon::MagnifyingGlass)) + .child(IconButton::new("inline-assist", Icon::MagicWand)), + ), + ) + .children(self.items.iter().map(|(child, _)| child.to_any())) } } From c14efb74d7477bbcc50e19b18c8988e2c3e571bf Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 14 Nov 2023 18:18:52 +0100 Subject: [PATCH 004/205] Finish up touchups for search UI. Co-authored-by: Nate --- crates/search2/src/buffer_search.rs | 62 +++++++++++++++++++---------- crates/search2/src/search.rs | 4 +- crates/workspace2/src/toolbar.rs | 3 ++ 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 72c9390511..83f1089887 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -117,7 +117,7 @@ impl Render for BufferSearchBar { // } // (None, None) => String::new(), // }; - let new_placeholder_text = Arc::from("Fix this up!"); + let new_placeholder_text = Arc::from("Search for.."); self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); @@ -172,18 +172,22 @@ impl Render for BufferSearchBar { cx, ) }; - div() - .border() - .border_color(blue()) - .flex() - .justify_between() + let should_show_replace_input = self.replace_enabled && supported_options.replacement; + let replace_all = should_show_replace_input.then(|| { + super::replace_action::(ReplaceAll, "Replace all", ui::Icon::ReplaceAll) + }); + let replace_next = should_show_replace_input + .then(|| super::replace_action::(ReplaceNext, "Replace next", ui::Icon::Replace)); + h_stack() + .w_full() + .p_1() .child( div() .flex() + .flex_1() .border_1() .border_color(red()) .rounded_md() - .w_96() .items_center() .child(IconElement::new(Icon::MagnifyingGlass)) .child(self.query_editor.clone()) @@ -198,21 +202,35 @@ impl Render for BufferSearchBar { .then(|| search_option_button(SearchOptions::WHOLE_WORD)), ), ) - .child(ButtonGroup::new(vec![ - search_button_for_mode(SearchMode::Text, Some(Side::Left), cx), - search_button_for_mode(SearchMode::Regex, Some(Side::Right), cx), - ])) - .when(supported_options.replacement, |this| { - this.child(super::toggle_replace_button(self.replace_enabled)) - }) - .when(self.replace_enabled, |this| { - this.child(div().w_80().child(self.replacement_editor.clone())) - }) - .children(match_count) - .child(nav_button_for_direction("<", Direction::Prev, cx)) - .child(nav_button_for_direction(">", Direction::Next, cx)) - .flex() - .justify_between() + .child( + h_stack() + .flex_none() + .child(ButtonGroup::new(vec![ + search_button_for_mode(SearchMode::Text, Some(Side::Left), cx), + search_button_for_mode(SearchMode::Regex, Some(Side::Right), cx), + ])) + .when(supported_options.replacement, |this| { + this.child(super::toggle_replace_button(self.replace_enabled)) + }), + ) + .child( + h_stack() + .gap_0p5() + .flex_1() + .when(self.replace_enabled, |this| { + this.child(self.replacement_editor.clone()) + .children(replace_next) + .children(replace_all) + }), + ) + .child( + h_stack() + .gap_0p5() + .flex_none() + .children(match_count) + .child(nav_button_for_direction("<", Direction::Prev, cx)) + .child(nav_button_for_direction(">", Direction::Next, cx)), + ) // let query_column = Flex::row() // .with_child( diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 955328c483..e4175e9e12 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -109,8 +109,10 @@ fn toggle_replace_button(active: bool) -> impl Component { fn replace_action( action: impl Action + 'static + Send + Sync, name: &'static str, + icon: ui::Icon, ) -> impl Component { - ui::IconButton::new(0, ui::Icon::Replace).on_click(move |_: &mut V, cx| { + // todo: add tooltip + ui::IconButton::new(0, icon).on_click(move |_: &mut V, cx| { cx.dispatch_action(action.boxed_clone()); }) } diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index e03e533314..c5ef75a32d 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -3,6 +3,7 @@ use gpui::{ div, AnyView, Component, Div, Entity, EntityId, EventEmitter, ParentElement, Render, Styled, View, ViewContext, WindowContext, }; +use theme2::ActiveTheme; use ui::{h_stack, v_stack, Button, Icon, IconButton, Label, LabelColor, StyledExt}; pub enum ToolbarItemEvent { @@ -81,6 +82,8 @@ impl Render for Toolbar { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { //dbg!(&self.items.len()); v_stack() + .border_b() + .border_color(cx.theme().colors().border) .child( h_stack() .justify_between() From c37faf0ab33c818398ba8de05be0fc5b7448890d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:05:48 +0100 Subject: [PATCH 005/205] Add query history and replace buttons --- crates/search2/src/buffer_search.rs | 215 ++++++++++++++-------------- crates/search2/src/search.rs | 5 +- crates/search2/src/search_bar.rs | 17 +-- 3 files changed, 113 insertions(+), 124 deletions(-) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index ccacff12ae..4e8584b804 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -11,8 +11,8 @@ use editor::Editor; use futures::channel::oneshot; use gpui::{ action, actions, div, red, Action, AppContext, Component, Div, EventEmitter, - ParentComponent as _, Render, Styled, Subscription, Task, View, ViewContext, - VisualContext as _, WindowContext, + InteractiveComponent, ParentComponent as _, Render, Styled, Subscription, Task, View, + ViewContext, VisualContext as _, WindowContext, }; use project::search::SearchQuery; use std::{any::Any, sync::Arc}; @@ -38,7 +38,6 @@ pub enum Event { } pub fn init(cx: &mut AppContext) { - dbg!("Registered"); cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) .detach(); } @@ -78,45 +77,51 @@ impl Render for BufferSearchBar { .map(|active_searchable_item| active_searchable_item.supported_options()) .unwrap_or_default(); - // let previous_query_keystrokes = - // cx.binding_for_action(&PreviousHistoryQuery {}) - // .map(|binding| { - // binding - // .keystrokes() - // .iter() - // .map(|k| k.to_string()) - // .collect::>() - // }); - // let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { - // binding - // .keystrokes() - // .iter() - // .map(|k| k.to_string()) - // .collect::>() - // }); - // let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { - // (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { - // format!( - // "Search ({}/{} for previous/next query)", - // previous_query_keystrokes.join(" "), - // next_query_keystrokes.join(" ") - // ) - // } - // (None, Some(next_query_keystrokes)) => { - // format!( - // "Search ({} for next query)", - // next_query_keystrokes.join(" ") - // ) - // } - // (Some(previous_query_keystrokes), None) => { - // format!( - // "Search ({} for previous query)", - // previous_query_keystrokes.join(" ") - // ) - // } - // (None, None) => String::new(), - // }; - let new_placeholder_text = Arc::from("Search for.."); + let previous_query_keystrokes = cx + .bindings_for_action(&PreviousHistoryQuery {}) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = cx + .bindings_for_action(&NextHistoryQuery {}) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + ) + } + (None, Some(next_query_keystrokes)) => { + format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + ) + } + (Some(previous_query_keystrokes), None) => { + format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + ) + } + (None, None) => String::new(), + }; + let new_placeholder_text = Arc::from(new_placeholder_text); self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); @@ -159,9 +164,9 @@ impl Render for BufferSearchBar { Some(ui::Label::new(message)) }); - let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + let nav_button_for_direction = |icon, direction, cx: &mut ViewContext| { render_nav_button( - label, + icon, direction, self.active_match_index.is_some(), move |this, cx| match direction { @@ -172,12 +177,32 @@ impl Render for BufferSearchBar { ) }; let should_show_replace_input = self.replace_enabled && supported_options.replacement; - let replace_all = should_show_replace_input.then(|| { - super::replace_action::(ReplaceAll, "Replace all", ui::Icon::ReplaceAll) - }); + let replace_all = should_show_replace_input + .then(|| super::render_replace_button::(ReplaceAll, ui::Icon::ReplaceAll)); let replace_next = should_show_replace_input - .then(|| super::replace_action::(ReplaceNext, "Replace next", ui::Icon::Replace)); + .then(|| super::render_replace_button::(ReplaceNext, ui::Icon::Replace)); + let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); + h_stack() + .key_context("BufferSearchBar") + .when(in_replace, |this| { + this.key_context("in_replace") + .on_action(Self::replace_next) + .on_action(Self::replace_all) + }) + .on_action(Self::previous_history_query) + .on_action(Self::next_history_query) + .when(supported_options.case, |this| { + this.on_action(Self::toggle_case_sensitive) + }) + .when(supported_options.word, |this| { + this.on_action(Self::toggle_whole_word) + }) + .when(supported_options.replacement, |this| { + this.on_action(Self::toggle_replace) + }) + .on_action(Self::select_next_match) + .on_action(Self::select_prev_match) .w_full() .p_1() .child( @@ -226,9 +251,18 @@ impl Render for BufferSearchBar { h_stack() .gap_0p5() .flex_none() + .child(self.render_action_button(cx)) .children(match_count) - .child(nav_button_for_direction("<", Direction::Prev, cx)) - .child(nav_button_for_direction(">", Direction::Next, cx)), + .child(nav_button_for_direction( + ui::Icon::ChevronLeft, + Direction::Prev, + cx, + )) + .child(nav_button_for_direction( + ui::Icon::ChevronRight, + Direction::Next, + cx, + )), ) // let query_column = Flex::row() @@ -343,7 +377,7 @@ impl ToolbarItemView for BufferSearchBar { cx.notify(); self.active_searchable_item_subscription.take(); self.active_searchable_item.take(); - dbg!("Take?"); + self.pending_search.take(); if let Some(searchable_item_handle) = @@ -382,7 +416,8 @@ impl BufferSearchBar { workspace.register_action(|workspace, a: &Deploy, cx| { workspace.active_pane().update(cx, |this, cx| { this.toolbar().update(cx, |this, cx| { - if this.item_of_type::().is_some() { + if let Some(search_bar) = this.item_of_type::() { + search_bar.update(cx, |this, cx| this.dismiss(&Dismiss, cx)); return; } let view = cx.build_view(|cx| BufferSearchBar::new(cx)); @@ -392,46 +427,8 @@ impl BufferSearchBar { }) }); }); - fn register_action( - workspace: &mut Workspace, - update: fn(&mut BufferSearchBar, &mut ViewContext<'_, BufferSearchBar>), - ) { - workspace.register_action(move |workspace, _: &A, cx| { - workspace.active_pane().update(cx, move |this, cx| { - this.toolbar().update(cx, move |toolbar, cx| { - let Some(search_bar) = toolbar.item_of_type::() else { - return; - }; - search_bar.update(cx, |this, cx| update(this, cx)) - }) - }); - }); - } - register_action::(workspace, |this, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) - }); - register_action::(workspace, |this: &mut BufferSearchBar, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, cx) - }); - register_action::(workspace, |this: &mut BufferSearchBar, cx| { - dbg!("Toggling"); - this.toggle_replace(&ToggleReplace, cx) - }); - // workspace.register_action(|workspace, _: &ToggleCaseSensitive, cx| { - // workspace.active_pane().update(cx, |this, cx| { - // this.toolbar().update(cx, |toolbar, cx| { - // let Some(search_bar) = toolbar.item_of_type::() else { - // return; - // }; - // search_bar.update(cx, |this, cx| { - // this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - // }) - // }) - // }); - // }); } pub fn new(cx: &mut ViewContext) -> Self { - dbg!("New"); let query_editor = cx.build_view(|cx| Editor::single_line(cx)); cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); @@ -463,7 +460,7 @@ impl BufferSearchBar { pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { self.dismissed = true; - dbg!("Dismissed :("); + for searchable_item in self.searchable_items_with_matches.keys() { if let Some(searchable_item) = WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) @@ -495,10 +492,9 @@ impl BufferSearchBar { pub fn show(&mut self, cx: &mut ViewContext) -> bool { if self.active_searchable_item.is_none() { - dbg!("Hey"); return false; } - dbg!("not dismissed"); + self.dismissed = false; cx.notify(); cx.emit(Event::UpdateLocation); @@ -590,13 +586,7 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_action_button( - &self, - icon: &'static str, - cx: &mut ViewContext, - ) -> impl Component { - let tooltip = "Select All Matches"; - let theme = cx.theme(); + fn render_action_button(&self, cx: &mut ViewContext) -> impl Component { // let tooltip_style = theme.tooltip.clone(); // let style = theme.search.action_button.clone(); @@ -695,8 +685,13 @@ impl BufferSearchBar { .searchable_items_with_matches .get(&searchable_item.downgrade()) { - let new_match_index = searchable_item - .match_index_for_direction(matches, index, direction, count, cx); + let new_match_index = searchable_item.match_index_for_direction( + matches, + index, + direction, + dbg!(count), + cx, + ); searchable_item.update_matches(matches, cx); searchable_item.activate_match(new_match_index, matches, cx); } @@ -769,6 +764,7 @@ impl BufferSearchBar { } fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext) { + dbg!(&event); match event { SearchEvent::MatchesInvalidated => { let _ = self.update_matches(cx); @@ -777,6 +773,12 @@ impl BufferSearchBar { } } + fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext) { + self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) + } + fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext) { + self.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + } fn clear_matches(&mut self, cx: &mut ViewContext) { let mut active_item_matches = None; for (searchable_item, matches) in self.searchable_items_with_matches.drain() { @@ -799,7 +801,7 @@ impl BufferSearchBar { let (done_tx, done_rx) = oneshot::channel(); let query = self.query(cx); self.pending_search.take(); - dbg!("update_matches"); + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); @@ -841,26 +843,23 @@ impl BufferSearchBar { .into(); self.active_search = Some(query.clone()); let query_text = query.as_str().to_string(); - dbg!(&query_text); + let matches = active_searchable_item.find_matches(query, cx); let active_searchable_item = active_searchable_item.downgrade(); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let matches = matches.await; - //dbg!(&matches); + this.update(&mut cx, |this, cx| { - dbg!("Updating!!"); if let Some(active_searchable_item) = WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) { - dbg!("in if!!"); this.searchable_items_with_matches .insert(active_searchable_item.downgrade(), matches); this.update_match_index(cx); this.search_history.add(query_text); if !this.dismissed { - dbg!("Not dismissed"); let matches = this .searchable_items_with_matches .get(&active_searchable_item.downgrade()) diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index e4175e9e12..233975839f 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -1,6 +1,6 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; -use gpui::{actions, Action, AnyElement, AppContext, Component, Element, Svg, View}; +use gpui::{actions, Action, AppContext, Component}; pub use mode::SearchMode; use project::search::SearchQuery; use ui::ButtonVariant; @@ -106,9 +106,8 @@ fn toggle_replace_button(active: bool) -> impl Component { .when(active, |button| button.variant(ButtonVariant::Filled)) } -fn replace_action( +fn render_replace_button( action: impl Action + 'static + Send + Sync, - name: &'static str, icon: ui::Icon, ) -> impl Component { // todo: add tooltip diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index 99735c5898..1c77a03741 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -1,28 +1,19 @@ use std::{borrow::Cow, sync::Arc}; use gpui::{div, Action, Component, ViewContext}; -use ui::{Button, ButtonVariant}; +use ui::{Button, ButtonVariant, IconButton}; use workspace::searchable::Direction; use crate::mode::{SearchMode, Side}; pub(super) fn render_nav_button( - icon: &'static str, + icon: ui::Icon, direction: Direction, + active: bool, on_click: impl Fn(&mut V, &mut ViewContext) + 'static + Send + Sync, cx: &mut ViewContext, ) -> impl Component { - let tooltip; - - match direction { - Direction::Prev => { - tooltip = "Select Previous Match"; - } - Direction::Next => { - tooltip = "Select Next Match"; - } - }; // let tooltip_style = cx.theme().tooltip.clone(); // let cursor_style = if active { // CursorStyle::PointingHand @@ -30,7 +21,7 @@ pub(super) fn render_nav_button( // CursorStyle::default() // }; // enum NavButton {} - Button::new(icon).on_click(Arc::new(on_click)) + IconButton::new("search-nav-button", icon).on_click(on_click) // MouseEventHandler::new::(direction as usize, cx, |state, cx| { // let theme = cx.theme(); // let style = theme From 69e01e67dcba9b52bceeefe5ab049012d4069056 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 15 Nov 2023 15:22:35 +0100 Subject: [PATCH 006/205] Bind cycle_mode action --- crates/search2/src/buffer_search.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 4e8584b804..7f2d2b0910 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -203,6 +203,7 @@ impl Render for BufferSearchBar { }) .on_action(Self::select_next_match) .on_action(Self::select_prev_match) + .on_action(Self::cycle_mode) .w_full() .p_1() .child( From 781a95d2e3e06d028f35c047b05cf023a4762049 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Nov 2023 14:47:45 +0100 Subject: [PATCH 007/205] Add back Completion::documentation --- crates/language2/src/buffer.rs | 41 ++++++++++++++++++++++++++++++ crates/language2/src/proto.rs | 1 + crates/project2/src/lsp_command.rs | 25 +++++++++++++++--- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 2c8c55d577..cd77dc5859 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -7,6 +7,7 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{language_settings, LanguageSettings}, + markdown::parse_markdown, outline::OutlineItem, syntax_map::{ SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, @@ -144,12 +145,52 @@ pub struct Diagnostic { pub is_unnecessary: bool, } +pub async fn prepare_completion_documentation( + documentation: &lsp::Documentation, + language_registry: &Arc, + language: Option>, +) -> Documentation { + match documentation { + lsp::Documentation::String(text) => { + if text.lines().count() <= 1 { + Documentation::SingleLine(text.clone()) + } else { + Documentation::MultiLinePlainText(text.clone()) + } + } + + lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { + lsp::MarkupKind::PlainText => { + if value.lines().count() <= 1 { + Documentation::SingleLine(value.clone()) + } else { + Documentation::MultiLinePlainText(value.clone()) + } + } + + lsp::MarkupKind::Markdown => { + let parsed = parse_markdown(value, language_registry, language).await; + Documentation::MultiLineMarkdown(parsed) + } + }, + } +} + +#[derive(Clone, Debug)] +pub enum Documentation { + Undocumented, + SingleLine(String), + MultiLinePlainText(String), + MultiLineMarkdown(ParsedMarkdown), +} + #[derive(Clone, Debug)] pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, pub server_id: LanguageServerId, + pub documentation: Option, pub lsp_completion: lsp::CompletionItem, } diff --git a/crates/language2/src/proto.rs b/crates/language2/src/proto.rs index c4abe39d47..957f4ee7fb 100644 --- a/crates/language2/src/proto.rs +++ b/crates/language2/src/proto.rs @@ -482,6 +482,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), + documentation: None, server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) diff --git a/crates/project2/src/lsp_command.rs b/crates/project2/src/lsp_command.rs index cc1821d3ff..94c277db1e 100644 --- a/crates/project2/src/lsp_command.rs +++ b/crates/project2/src/lsp_command.rs @@ -10,7 +10,7 @@ use futures::future; use gpui::{AppContext, AsyncAppContext, Model}; use language::{ language_settings::{language_settings, InlayHintKind}, - point_from_lsp, point_to_lsp, + point_from_lsp, point_to_lsp, prepare_completion_documentation, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, @@ -1339,7 +1339,7 @@ impl LspCommand for GetCompletions { async fn response_from_lsp( self, completions: Option, - _: Model, + project: Model, buffer: Model, server_id: LanguageServerId, mut cx: AsyncAppContext, @@ -1359,7 +1359,8 @@ impl LspCommand for GetCompletions { Default::default() }; - let completions = buffer.update(&mut cx, |buffer, _| { + let completions = buffer.update(&mut cx, |buffer, cx| { + let language_registry = project.read(cx).languages().clone(); let language = buffer.language().cloned(); let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); @@ -1443,14 +1444,29 @@ impl LspCommand for GetCompletions { } }; + let language_registry = language_registry.clone(); let language = language.clone(); LineEnding::normalize(&mut new_text); Some(async move { let mut label = None; - if let Some(language) = language { + if let Some(language) = language.as_ref() { language.process_completion(&mut lsp_completion).await; label = language.label_for_completion(&lsp_completion).await; } + + let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { + Some( + prepare_completion_documentation( + lsp_docs, + &language_registry, + language.clone(), + ) + .await, + ) + } else { + None + }; + Completion { old_range, new_text, @@ -1460,6 +1476,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), + documentation, server_id, lsp_completion, } From c08ce1c3b86e95603da73d226c0945278b03ac17 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 16 Nov 2023 14:55:06 +0100 Subject: [PATCH 008/205] Start rendering autocompletion menu --- crates/editor2/src/editor.rs | 477 +++++++++++++++++------------------ 1 file changed, 233 insertions(+), 244 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 8e7bd5876f..48c61c0f32 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -54,13 +54,13 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape, - Diagnostic, IndentKind, IndentSize, Language, LanguageRegistry, LanguageServerName, - OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, LanguageRegistry, + LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; use lazy_static::lazy_static; use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; -use lsp::{DiagnosticSeverity, Documentation, LanguageServerId}; +use lsp::{DiagnosticSeverity, LanguageServerId}; use movement::TextLayoutDetails; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip}; +use ui::{h_stack, v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::{ItemEvent, ItemHandle}, @@ -1224,208 +1224,196 @@ impl CompletionsMenu { workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { - todo!("old implementation below") + // enum CompletionTag {} + + let settings = EditorSettings::get_global(cx); + let show_completion_documentation = settings.show_completion_documentation; + + let widest_completion_ix = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; + let documentation = &completion.documentation; + + let mut len = completion.label.text.chars().count(); + if let Some(Documentation::SingleLine(text)) = documentation { + if show_completion_documentation { + len += text.chars().count(); + } + } + + len + }) + .map(|(ix, _)| ix); + + let completions = self.completions.clone(); + let matches = self.matches.clone(); + let selected_item = self.selected_item; + + let list = uniform_list("completions", matches.len(), move |editor, range, cx| { + let start_ix = range.start; + let completions_guard = completions.read(); + + matches[range] + .iter() + .enumerate() + .map(|(ix, mat)| { + let item_ix = start_ix + ix; + let candidate_id = mat.candidate_id; + let completion = &completions_guard[candidate_id]; + + let documentation = if show_completion_documentation { + &completion.documentation + } else { + &None + }; + + // todo!("highlights") + // let highlights = combine_syntax_and_fuzzy_match_highlights( + // &completion.label.text, + // style.text.color.into(), + // styled_runs_for_code_label(&completion.label, &style.syntax), + // &mat.positions, + // ) + + // todo!("documentation") + // MouseEventHandler::new::(mat.candidate_id, cx, |state, _| { + // let completion_label = HighlightedLabel::new( + // completion.label.text.clone(), + // combine_syntax_and_fuzzy_match_highlights( + // &completion.label.text, + // style.text.color.into(), + // styled_runs_for_code_label(&completion.label, &style.syntax), + // &mat.positions, + // ), + // ); + // Text::new(completion.label.text.clone(), style.text.clone()) + // .with_soft_wrap(false) + // .with_highlights(); + + // if let Some(Documentation::SingleLine(text)) = documentation { + // h_stack() + // .child(completion_label) + // .with_children((|| { + // let text_style = TextStyle { + // color: style.autocomplete.inline_docs_color, + // font_size: style.text.font_size + // * style.autocomplete.inline_docs_size_percent, + // ..style.text.clone() + // }; + + // let label = Text::new(text.clone(), text_style) + // .aligned() + // .constrained() + // .dynamically(move |constraint, _, _| gpui::SizeConstraint { + // min: constraint.min, + // max: vec2f(constraint.max.x(), constraint.min.y()), + // }); + + // if Some(item_ix) == widest_completion_ix { + // Some( + // label + // .contained() + // .with_style(style.autocomplete.inline_docs_container) + // .into_any(), + // ) + // } else { + // Some(label.flex_float().into_any()) + // } + // })()) + // .into_any() + // } else { + // completion_label.into_any() + // } + // .contained() + // .with_style(item_style) + // .constrained() + // .dynamically(move |constraint, _, _| { + // if Some(item_ix) == widest_completion_ix { + // constraint + // } else { + // gpui::SizeConstraint { + // min: constraint.min, + // max: constraint.min, + // } + // } + // }) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_down(MouseButton::Left, move |_, this, cx| { + // this.confirm_completion( + // &ConfirmCompletion { + // item_ix: Some(item_ix), + // }, + // cx, + // ) + // .map(|task| task.detach()); + // }) + // .constrained() + // + div() + .id(mat.candidate_id) + .bg(gpui::green()) + .hover(|style| style.bg(gpui::blue())) + .when(item_ix == selected_item, |div| div.bg(gpui::blue())) + .child(completion.label.text.clone()) + .min_w(px(300.)) + .max_w(px(700.)) + }) + .collect() + }) + .with_width_from_item(widest_completion_ix); + + list.render() + // todo!("multiline documentation") + // enum MultiLineDocumentation {} + + // Flex::row() + // .with_child(list.flex(1., false)) + // .with_children({ + // let mat = &self.matches[selected_item]; + // let completions = self.completions.read(); + // let completion = &completions[mat.candidate_id]; + // let documentation = &completion.documentation; + + // match documentation { + // Some(Documentation::MultiLinePlainText(text)) => Some( + // Flex::column() + // .scrollable::(0, None, cx) + // .with_child( + // Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), + // ) + // .contained() + // .with_style(style.autocomplete.alongside_docs_container) + // .constrained() + // .with_max_width(style.autocomplete.alongside_docs_max_width) + // .flex(1., false), + // ), + + // Some(Documentation::MultiLineMarkdown(parsed)) => Some( + // Flex::column() + // .scrollable::(0, None, cx) + // .with_child(render_parsed_markdown::( + // parsed, &style, workspace, cx, + // )) + // .contained() + // .with_style(style.autocomplete.alongside_docs_container) + // .constrained() + // .with_max_width(style.autocomplete.alongside_docs_max_width) + // .flex(1., false), + // ), + + // _ => None, + // } + // }) + // .contained() + // .with_style(style.autocomplete.container) + // .into_any() } - // enum CompletionTag {} - - // let settings = EditorSettings>(cx); - // let show_completion_documentation = settings.show_completion_documentation; - - // let widest_completion_ix = self - // .matches - // .iter() - // .enumerate() - // .max_by_key(|(_, mat)| { - // let completions = self.completions.read(); - // let completion = &completions[mat.candidate_id]; - // let documentation = &completion.documentation; - - // let mut len = completion.label.text.chars().count(); - // if let Some(Documentation::SingleLine(text)) = documentation { - // if show_completion_documentation { - // len += text.chars().count(); - // } - // } - - // len - // }) - // .map(|(ix, _)| ix); - - // let completions = self.completions.clone(); - // let matches = self.matches.clone(); - // let selected_item = self.selected_item; - - // let list = UniformList::new(self.list.clone(), matches.len(), cx, { - // let style = style.clone(); - // move |_, range, items, cx| { - // let start_ix = range.start; - // let completions_guard = completions.read(); - - // for (ix, mat) in matches[range].iter().enumerate() { - // let item_ix = start_ix + ix; - // let candidate_id = mat.candidate_id; - // let completion = &completions_guard[candidate_id]; - - // let documentation = if show_completion_documentation { - // &completion.documentation - // } else { - // &None - // }; - - // items.push( - // MouseEventHandler::new::( - // mat.candidate_id, - // cx, - // |state, _| { - // let item_style = if item_ix == selected_item { - // style.autocomplete.selected_item - // } else if state.hovered() { - // style.autocomplete.hovered_item - // } else { - // style.autocomplete.item - // }; - - // let completion_label = - // Text::new(completion.label.text.clone(), style.text.clone()) - // .with_soft_wrap(false) - // .with_highlights( - // combine_syntax_and_fuzzy_match_highlights( - // &completion.label.text, - // style.text.color.into(), - // styled_runs_for_code_label( - // &completion.label, - // &style.syntax, - // ), - // &mat.positions, - // ), - // ); - - // if let Some(Documentation::SingleLine(text)) = documentation { - // Flex::row() - // .with_child(completion_label) - // .with_children((|| { - // let text_style = TextStyle { - // color: style.autocomplete.inline_docs_color, - // font_size: style.text.font_size - // * style.autocomplete.inline_docs_size_percent, - // ..style.text.clone() - // }; - - // let label = Text::new(text.clone(), text_style) - // .aligned() - // .constrained() - // .dynamically(move |constraint, _, _| { - // gpui::SizeConstraint { - // min: constraint.min, - // max: vec2f( - // constraint.max.x(), - // constraint.min.y(), - // ), - // } - // }); - - // if Some(item_ix) == widest_completion_ix { - // Some( - // label - // .contained() - // .with_style( - // style - // .autocomplete - // .inline_docs_container, - // ) - // .into_any(), - // ) - // } else { - // Some(label.flex_float().into_any()) - // } - // })()) - // .into_any() - // } else { - // completion_label.into_any() - // } - // .contained() - // .with_style(item_style) - // .constrained() - // .dynamically( - // move |constraint, _, _| { - // if Some(item_ix) == widest_completion_ix { - // constraint - // } else { - // gpui::SizeConstraint { - // min: constraint.min, - // max: constraint.min, - // } - // } - // }, - // ) - // }, - // ) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // this.confirm_completion( - // &ConfirmCompletion { - // item_ix: Some(item_ix), - // }, - // cx, - // ) - // .map(|task| task.detach()); - // }) - // .constrained() - // .with_min_width(style.autocomplete.completion_min_width) - // .with_max_width(style.autocomplete.completion_max_width) - // .into_any(), - // ); - // } - // } - // }) - // .with_width_from_item(widest_completion_ix); - - // enum MultiLineDocumentation {} - - // Flex::row() - // .with_child(list.flex(1., false)) - // .with_children({ - // let mat = &self.matches[selected_item]; - // let completions = self.completions.read(); - // let completion = &completions[mat.candidate_id]; - // let documentation = &completion.documentation; - - // match documentation { - // Some(Documentation::MultiLinePlainText(text)) => Some( - // Flex::column() - // .scrollable::(0, None, cx) - // .with_child( - // Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), - // ) - // .contained() - // .with_style(style.autocomplete.alongside_docs_container) - // .constrained() - // .with_max_width(style.autocomplete.alongside_docs_max_width) - // .flex(1., false), - // ), - - // Some(Documentation::MultiLineMarkdown(parsed)) => Some( - // Flex::column() - // .scrollable::(0, None, cx) - // .with_child(render_parsed_markdown::( - // parsed, &style, workspace, cx, - // )) - // .contained() - // .with_style(style.autocomplete.alongside_docs_container) - // .constrained() - // .with_max_width(style.autocomplete.alongside_docs_max_width) - // .flex(1., false), - // ), - - // _ => None, - // } - // }) - // .contained() - // .with_style(style.autocomplete.container) - // .into_any() - // } - pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) { let mut matches = if let Some(query) = query { fuzzy::match_strings( @@ -10110,49 +10098,50 @@ pub fn combine_syntax_and_fuzzy_match_highlights( result } -// pub fn styled_runs_for_code_label<'a>( -// label: &'a CodeLabel, -// syntax_theme: &'a theme::SyntaxTheme, -// ) -> impl 'a + Iterator, HighlightStyle)> { -// let fade_out = HighlightStyle { -// fade_out: Some(0.35), -// ..Default::default() -// }; +pub fn styled_runs_for_code_label<'a>( + label: &'a CodeLabel, + syntax_theme: &'a theme::SyntaxTheme, +) -> impl 'a + Iterator, HighlightStyle)> { + let fade_out = HighlightStyle { + fade_out: Some(0.35), + ..Default::default() + }; -// let mut prev_end = label.filter_range.end; -// label -// .runs -// .iter() -// .enumerate() -// .flat_map(move |(ix, (range, highlight_id))| { -// let style = if let Some(style) = highlight_id.style(syntax_theme) { -// style -// } else { -// return Default::default(); -// }; -// let mut muted_style = style; -// muted_style.highlight(fade_out); + let mut prev_end = label.filter_range.end; + label + .runs + .iter() + .enumerate() + .flat_map(move |(ix, (range, highlight_id))| { + let style = if let Some(style) = highlight_id.style(syntax_theme) { + style + } else { + return Default::default(); + }; + let mut muted_style = style; + muted_style.highlight(fade_out); -// let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); -// if range.start >= label.filter_range.end { -// if range.start > prev_end { -// runs.push((prev_end..range.start, fade_out)); -// } -// runs.push((range.clone(), muted_style)); -// } else if range.end <= label.filter_range.end { -// runs.push((range.clone(), style)); -// } else { -// runs.push((range.start..label.filter_range.end, style)); -// runs.push((label.filter_range.end..range.end, muted_style)); -// } -// prev_end = cmp::max(prev_end, range.end); + let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); + if range.start >= label.filter_range.end { + if range.start > prev_end { + runs.push((prev_end..range.start, fade_out)); + } + runs.push((range.clone(), muted_style)); + } else if range.end <= label.filter_range.end { + runs.push((range.clone(), style)); + } else { + runs.push((range.start..label.filter_range.end, style)); + runs.push((label.filter_range.end..range.end, muted_style)); + } + prev_end = cmp::max(prev_end, range.end); -// if ix + 1 == label.runs.len() && label.text.len() > prev_end { -// runs.push((prev_end..label.text.len(), fade_out)); -// } + if ix + 1 == label.runs.len() && label.text.len() > prev_end { + runs.push((prev_end..label.text.len(), fade_out)); + } -// runs -// }) + runs + }) +} pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator + 'a { let mut index = 0; From f8b91bd0f0d64b41e18af9121dd4864bfddb52f2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:36:19 +0100 Subject: [PATCH 009/205] Fix some of the warnings --- crates/search2/src/buffer_search.rs | 89 +++++++++++------ crates/search2/src/mode.rs | 6 -- crates/search2/src/search_bar.rs | 111 +-------------------- crates/terminal_view2/src/terminal_view.rs | 2 +- 4 files changed, 58 insertions(+), 150 deletions(-) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 7f2d2b0910..021cc57015 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -1,10 +1,10 @@ use crate::{ history::SearchHistory, - mode::{next_mode, SearchMode, Side}, + mode::{next_mode, SearchMode}, search_bar::{render_nav_button, render_search_mode_button}, - CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, - SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, - ToggleWholeWord, + ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, + ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, + ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, }; use collections::HashMap; use editor::Editor; @@ -16,7 +16,6 @@ use gpui::{ }; use project::search::SearchQuery; use std::{any::Any, sync::Arc}; -use theme::ActiveTheme; use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement}; use util::ResultExt; @@ -129,18 +128,12 @@ impl Render for BufferSearchBar { editor.set_placeholder_text("Replace with...", cx); }); - let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + let search_button_for_mode = |mode| { let is_active = self.current_mode == mode; - render_search_mode_button( - mode, - side, - is_active, - move |this, cx| { - this.activate_search_mode(mode, cx); - }, - cx, - ) + render_search_mode_button(mode, is_active, move |this: &mut Self, cx| { + this.activate_search_mode(mode, cx); + }) }; let search_option_button = |option| { let is_active = self.search_options.contains(option); @@ -164,16 +157,14 @@ impl Render for BufferSearchBar { Some(ui::Label::new(message)) }); - let nav_button_for_direction = |icon, direction, cx: &mut ViewContext| { + let nav_button_for_direction = |icon, direction| { render_nav_button( icon, - direction, self.active_match_index.is_some(), - move |this, cx| match direction { + move |this: &mut Self, cx| match direction { Direction::Prev => this.select_prev_match(&Default::default(), cx), Direction::Next => this.select_next_match(&Default::default(), cx), }, - cx, ) }; let should_show_replace_input = self.replace_enabled && supported_options.replacement; @@ -231,8 +222,8 @@ impl Render for BufferSearchBar { h_stack() .flex_none() .child(ButtonGroup::new(vec![ - search_button_for_mode(SearchMode::Text, Some(Side::Left), cx), - search_button_for_mode(SearchMode::Regex, Some(Side::Right), cx), + search_button_for_mode(SearchMode::Text), + search_button_for_mode(SearchMode::Regex), ])) .when(supported_options.replacement, |this| { this.child(super::toggle_replace_button(self.replace_enabled)) @@ -252,17 +243,15 @@ impl Render for BufferSearchBar { h_stack() .gap_0p5() .flex_none() - .child(self.render_action_button(cx)) + .child(self.render_action_button()) .children(match_count) .child(nav_button_for_direction( ui::Icon::ChevronLeft, Direction::Prev, - cx, )) .child(nav_button_for_direction( ui::Icon::ChevronRight, Direction::Next, - cx, )), ) @@ -428,6 +417,46 @@ impl BufferSearchBar { }) }); }); + fn register_action( + workspace: &mut Workspace, + update: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ) { + workspace.register_action(move |workspace, action: &A, cx| { + workspace.active_pane().update(cx, move |this, cx| { + this.toolbar().update(cx, move |this, cx| { + if let Some(search_bar) = this.item_of_type::() { + search_bar.update(cx, move |this, cx| update(this, action, cx)); + cx.notify(); + } + }) + }); + }); + } + + register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { + this.toggle_case_sensitive(action, cx); + }); + register_action(workspace, |this, action: &ToggleWholeWord, cx| { + this.toggle_whole_word(action, cx); + }); + register_action(workspace, |this, action: &ToggleReplace, cx| { + this.toggle_replace(action, cx); + }); + register_action(workspace, |this, action: &ActivateRegexMode, cx| { + this.activate_search_mode(SearchMode::Regex, cx); + }); + register_action(workspace, |this, action: &ActivateTextMode, cx| { + this.activate_search_mode(SearchMode::Text, cx); + }); + register_action(workspace, |this, action: &SelectNextMatch, cx| { + this.select_next_match(action, cx); + }); + register_action(workspace, |this, action: &SelectPrevMatch, cx| { + this.select_prev_match(action, cx); + }); + register_action(workspace, |this, action: &SelectAllMatches, cx| { + this.select_all_matches(action, cx); + }); } pub fn new(cx: &mut ViewContext) -> Self { let query_editor = cx.build_view(|cx| Editor::single_line(cx)); @@ -587,7 +616,7 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_action_button(&self, cx: &mut ViewContext) -> impl Component { + fn render_action_button(&self) -> impl Component { // let tooltip_style = theme.tooltip.clone(); // let style = theme.search.action_button.clone(); @@ -686,13 +715,8 @@ impl BufferSearchBar { .searchable_items_with_matches .get(&searchable_item.downgrade()) { - let new_match_index = searchable_item.match_index_for_direction( - matches, - index, - direction, - dbg!(count), - cx, - ); + let new_match_index = searchable_item + .match_index_for_direction(matches, index, direction, count, cx); searchable_item.update_matches(matches, cx); searchable_item.activate_match(new_match_index, matches, cx); } @@ -765,7 +789,6 @@ impl BufferSearchBar { } fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext) { - dbg!(&event); match event { SearchEvent::MatchesInvalidated => { let _ = self.update_matches(cx); diff --git a/crates/search2/src/mode.rs b/crates/search2/src/mode.rs index 8afc2bd3f4..bb729cb6c0 100644 --- a/crates/search2/src/mode.rs +++ b/crates/search2/src/mode.rs @@ -10,12 +10,6 @@ pub enum SearchMode { Regex, } -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) enum Side { - Left, - Right, -} - impl SearchMode { pub(crate) fn label(&self) -> &'static str { match self { diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index 1c77a03741..46a3357763 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -4,15 +4,12 @@ use gpui::{div, Action, Component, ViewContext}; use ui::{Button, ButtonVariant, IconButton}; use workspace::searchable::Direction; -use crate::mode::{SearchMode, Side}; +use crate::mode::SearchMode; pub(super) fn render_nav_button( icon: ui::Icon, - direction: Direction, - active: bool, on_click: impl Fn(&mut V, &mut ViewContext) + 'static + Send + Sync, - cx: &mut ViewContext, ) -> impl Component { // let tooltip_style = cx.theme().tooltip.clone(); // let cursor_style = if active { @@ -22,57 +19,13 @@ pub(super) fn render_nav_button( // }; // enum NavButton {} IconButton::new("search-nav-button", icon).on_click(on_click) - // MouseEventHandler::new::(direction as usize, cx, |state, cx| { - // let theme = cx.theme(); - // let style = theme - // .search - // .nav_button - // .in_state(active) - // .style_for(state) - // .clone(); - // let mut container_style = style.container.clone(); - // let label = Label::new(icon, style.label.clone()).aligned().contained(); - // container_style.corner_radii = match direction { - // Direction::Prev => CornerRadii { - // bottom_right: 0., - // top_right: 0., - // ..container_style.corner_radii - // }, - // Direction::Next => CornerRadii { - // bottom_left: 0., - // top_left: 0., - // ..container_style.corner_radii - // }, - // }; - // if direction == Direction::Prev { - // // Remove right border so that when both Next and Prev buttons are - // // next to one another, there's no double border between them. - // container_style.border.right = false; - // } - // label.with_style(container_style) - // }) - // .on_click(MouseButton::Left, on_click) - // .with_cursor_style(cursor_style) - // .with_tooltip::( - // direction as usize, - // tooltip.to_string(), - // Some(action), - // tooltip_style, - // cx, - // ) - // .into_any() } pub(crate) fn render_search_mode_button( mode: SearchMode, - side: Option, is_active: bool, on_click: impl Fn(&mut V, &mut ViewContext) + 'static + Send + Sync, - cx: &mut ViewContext, ) -> Button { - //let tooltip_style = cx.theme().tooltip.clone(); - enum SearchModeButton {} - let button_variant = if is_active { ButtonVariant::Filled } else { @@ -82,66 +35,6 @@ pub(crate) fn render_search_mode_button( Button::new(mode.label()) .on_click(Arc::new(on_click)) .variant(button_variant) - - // v_stack() - // .border_2() - // .rounded_md() - // .when(side == Some(Side::Left), |this| { - // this.border_r_0().rounded_tr_none().rounded_br_none() - // }) - // .when(side == Some(Side::Right), |this| { - // this.border_l_0().rounded_bl_none().rounded_tl_none() - // }) - // .on_key_down(move |v, _, _, cx| on_click(v, cx)) - // .child(Label::new(mode.label())) - // MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { - // let theme = cx.theme(); - // let style = theme - // .search - // .mode_button - // .in_state(is_active) - // .style_for(state) - // .clone(); - - // let mut container_style = style.container; - // if let Some(button_side) = side { - // if button_side == Side::Left { - // container_style.border.left = true; - // container_style.corner_radii = CornerRadii { - // bottom_right: 0., - // top_right: 0., - // ..container_style.corner_radii - // }; - // } else { - // container_style.border.left = false; - // container_style.corner_radii = CornerRadii { - // bottom_left: 0., - // top_left: 0., - // ..container_style.corner_radii - // }; - // } - // } else { - // container_style.border.left = false; - // container_style.corner_radii = CornerRadii::default(); - // } - - // Label::new(mode.label(), style.text) - // .aligned() - // .contained() - // .with_style(container_style) - // .constrained() - // .with_height(theme.search.search_bar_row_height) - // }) - // .on_click(MouseButton::Left, on_click) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // mode.region_id(), - // mode.tooltip_text().to_owned(), - // Some(mode.activate_action()), - // tooltip_style, - // cx, - // ) - // .into_any() } pub(crate) fn render_option_button_icon( @@ -150,8 +43,6 @@ pub(crate) fn render_option_button_icon( id: usize, label: impl Into>, action: Box, - //on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, - cx: &mut ViewContext, ) -> impl Component { //let tooltip_style = cx.theme().tooltip.clone(); div() diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 14391ca2b2..2ed7c8f472 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -792,7 +792,7 @@ impl Item for TerminalView { // } fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft { flex: None } + ToolbarItemLocation::PrimaryLeft } fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option> { From 8845f5a1831bf83421fd6e888fb510110e4e36e7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:22:07 +0100 Subject: [PATCH 010/205] Clean up warnings --- crates/search2/src/buffer_search.rs | 183 +--------------------------- crates/search2/src/search_bar.rs | 14 +-- 2 files changed, 12 insertions(+), 185 deletions(-) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 021cc57015..3c995af20b 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -254,107 +254,6 @@ impl Render for BufferSearchBar { Direction::Next, )), ) - - // let query_column = Flex::row() - // .with_child( - // Svg::for_style(theme.search.editor_icon.clone().icon) - // .contained() - // .with_style(theme.search.editor_icon.clone().container), - // ) - // .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) - // .with_child( - // Flex::row() - // .with_children( - // supported_options - // .case - // .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)), - // ) - // .with_children( - // supported_options - // .word - // .then(|| search_option_button(SearchOptions::WHOLE_WORD)), - // ) - // .flex_float() - // .contained(), - // ) - // .align_children_center() - // .contained() - // .with_style(query_container_style) - // .constrained() - // .with_min_width(theme.search.editor.min_width) - // .with_max_width(theme.search.editor.max_width) - // .with_height(theme.search.search_bar_row_height) - // .flex(1., false); - // let should_show_replace_input = self.replace_enabled && supported_options.replacement; - - // let replacement = should_show_replace_input.then(|| { - // div() - // .child( - // Svg::for_style(theme.search.replace_icon.clone().icon) - // .contained() - // .with_style(theme.search.replace_icon.clone().container), - // ) - // .child(self.replacement_editor) - // .align_children_center() - // .flex(1., true) - // .contained() - // .with_style(query_container_style) - // .constrained() - // .with_min_width(theme.search.editor.min_width) - // .with_max_width(theme.search.editor.max_width) - // .with_height(theme.search.search_bar_row_height) - // .flex(1., false) - // }); - // let replace_all = - // should_show_replace_input.then(|| super::replace_action(ReplaceAll, "Replace all")); - // let replace_next = - // should_show_replace_input.then(|| super::replace_action(ReplaceNext, "Replace next")); - // let switches_column = supported_options.replacement.then(|| { - // Flex::row() - // .align_children_center() - // .with_child(super::toggle_replace_button(self.replace_enabled)) - // .constrained() - // .with_height(theme.search.search_bar_row_height) - // .contained() - // .with_style(theme.search.option_button_group) - // }); - // let mode_column = div() - // .child(search_button_for_mode( - // SearchMode::Text, - // Some(Side::Left), - // cx, - // )) - // .child(search_button_for_mode( - // SearchMode::Regex, - // Some(Side::Right), - // cx, - // )) - // .contained() - // .with_style(theme.search.modes_container) - // .constrained() - // .with_height(theme.search.search_bar_row_height); - - // let nav_column = div() - // .align_children_center() - // .with_children(replace_next) - // .with_children(replace_all) - // .with_child(self.render_action_button("icons/select-all.svg", cx)) - // .with_child(div().children(match_count)) - // .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - // .with_child(nav_button_for_direction(">", Direction::Next, cx)) - // .constrained() - // .with_height(theme.search.search_bar_row_height) - // .flex_float(); - - // div() - // .child(query_column) - // .child(mode_column) - // .children(switches_column) - // .children(replacement) - // .child(nav_column) - // .contained() - // .with_style(theme.search.container) - // .into_any_named("search bar") } } @@ -442,12 +341,15 @@ impl BufferSearchBar { register_action(workspace, |this, action: &ToggleReplace, cx| { this.toggle_replace(action, cx); }); - register_action(workspace, |this, action: &ActivateRegexMode, cx| { + register_action(workspace, |this, _: &ActivateRegexMode, cx| { this.activate_search_mode(SearchMode::Regex, cx); }); - register_action(workspace, |this, action: &ActivateTextMode, cx| { + register_action(workspace, |this, _: &ActivateTextMode, cx| { this.activate_search_mode(SearchMode::Text, cx); }); + register_action(workspace, |this, action: &CycleMode, cx| { + this.cycle_mode(action, cx) + }); register_action(workspace, |this, action: &SelectNextMatch, cx| { this.select_next_match(action, cx); }); @@ -639,20 +541,6 @@ impl BufferSearchBar { cx.notify(); } - fn deploy_bar(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { - let mut propagate_action = true; - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if search_bar.deploy(action, cx) { - propagate_action = false; - } - }); - } - if !propagate_action { - cx.stop_propagation(); - } - } - fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext) { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { if !search_bar.read(cx).dismissed { @@ -740,36 +628,6 @@ impl BufferSearchBar { } } - fn select_next_match_on_pane( - pane: &mut Pane, - action: &SelectNextMatch, - cx: &mut ViewContext, - ) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_next_match(action, cx)); - } - } - - fn select_prev_match_on_pane( - pane: &mut Pane, - action: &SelectPrevMatch, - cx: &mut ViewContext, - ) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_prev_match(action, cx)); - } - } - - fn select_all_matches_on_pane( - pane: &mut Pane, - action: &SelectAllMatches, - cx: &mut ViewContext, - ) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_all_matches(action, cx)); - } - } - fn on_query_editor_event( &mut self, _: View, @@ -941,23 +799,6 @@ impl BufferSearchBar { fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { self.activate_search_mode(next_mode(&self.current_mode, false), cx); } - fn cycle_mode_on_pane(pane: &mut Pane, action: &CycleMode, cx: &mut ViewContext) { - let mut should_propagate = true; - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| { - if bar.show(cx) { - should_propagate = false; - bar.cycle_mode(action, cx); - false - } else { - true - } - }); - } - if !should_propagate { - cx.stop_propagation(); - } - } fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { if let Some(_) = &self.active_searchable_item { self.replace_enabled = !self.replace_enabled; @@ -1037,20 +878,6 @@ impl BufferSearchBar { } } } - fn replace_next_on_pane(pane: &mut Pane, action: &ReplaceNext, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.replace_next(action, cx)); - cx.stop_propagation(); - return; - } - } - fn replace_all_on_pane(pane: &mut Pane, action: &ReplaceAll, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.replace_all(action, cx)); - cx.stop_propagation(); - return; - } - } } #[cfg(test)] diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index 46a3357763..ddd844be1d 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -2,13 +2,13 @@ use std::{borrow::Cow, sync::Arc}; use gpui::{div, Action, Component, ViewContext}; use ui::{Button, ButtonVariant, IconButton}; -use workspace::searchable::Direction; + use crate::mode::SearchMode; pub(super) fn render_nav_button( icon: ui::Icon, - active: bool, + _active: bool, on_click: impl Fn(&mut V, &mut ViewContext) + 'static + Send + Sync, ) -> impl Component { // let tooltip_style = cx.theme().tooltip.clone(); @@ -38,11 +38,11 @@ pub(crate) fn render_search_mode_button( } pub(crate) fn render_option_button_icon( - is_active: bool, - icon: &'static str, - id: usize, - label: impl Into>, - action: Box, + _is_active: bool, + _icon: &'static str, + _id: usize, + _label: impl Into>, + _action: Box, ) -> impl Component { //let tooltip_style = cx.theme().tooltip.clone(); div() From f23cc724d44c08ebc63ebfcfdb57560208073a27 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:23:42 +0100 Subject: [PATCH 011/205] chore: cargo fmt --- crates/search2/src/search_bar.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index ddd844be1d..8879644eab 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -3,7 +3,6 @@ use std::{borrow::Cow, sync::Arc}; use gpui::{div, Action, Component, ViewContext}; use ui::{Button, ButtonVariant, IconButton}; - use crate::mode::SearchMode; pub(super) fn render_nav_button( From 741e11cc11df7f5b84d7dce8db7f20610655bd9b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:27:33 +0100 Subject: [PATCH 012/205] Fix up action derive --- crates/search2/src/buffer_search.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 3c995af20b..5e21380629 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -10,11 +10,12 @@ use collections::HashMap; use editor::Editor; use futures::channel::oneshot; use gpui::{ - action, actions, div, red, Action, AppContext, Component, Div, EventEmitter, - InteractiveComponent, ParentComponent as _, Render, Styled, Subscription, Task, View, - ViewContext, VisualContext as _, WindowContext, + actions, div, red, Action, AppContext, Component, Div, EventEmitter, InteractiveComponent, + ParentComponent as _, Render, Styled, Subscription, Task, View, ViewContext, + VisualContext as _, WindowContext, }; use project::search::SearchQuery; +use serde::Deserialize; use std::{any::Any, sync::Arc}; use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement}; @@ -25,7 +26,7 @@ use workspace::{ Pane, ToolbarItemLocation, ToolbarItemView, Workspace, }; -#[action] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct Deploy { pub focus: bool, } From 27600b6b8d921bc2584b7ef21f6a2b2b07ba3c48 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 17 Nov 2023 14:42:20 +0100 Subject: [PATCH 013/205] Remove dead code (for now). Ensure actions are registed just once (previously some were registered on both Workspace and search bar itself). --- crates/search2/src/buffer_search.rs | 88 ++++++++++------------------- crates/search2/src/mode.rs | 24 -------- crates/search2/src/search_bar.rs | 32 ----------- 3 files changed, 30 insertions(+), 114 deletions(-) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 5e21380629..d1f9103b67 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -23,7 +23,7 @@ use util::ResultExt; use workspace::{ item::ItemHandle, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, - Pane, ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemLocation, ToolbarItemView, Workspace, }; #[derive(PartialEq, Clone, Deserialize, Default, Action)] @@ -71,11 +71,7 @@ impl Render for BufferSearchBar { // } else { // theme.search.editor.input.container // }; - let supported_options = self - .active_searchable_item - .as_ref() - .map(|active_searchable_item| active_searchable_item.supported_options()) - .unwrap_or_default(); + let supported_options = self.supported_options(); let previous_query_keystrokes = cx .bindings_for_action(&PreviousHistoryQuery {}) @@ -184,18 +180,6 @@ impl Render for BufferSearchBar { }) .on_action(Self::previous_history_query) .on_action(Self::next_history_query) - .when(supported_options.case, |this| { - this.on_action(Self::toggle_case_sensitive) - }) - .when(supported_options.word, |this| { - this.on_action(Self::toggle_whole_word) - }) - .when(supported_options.replacement, |this| { - this.on_action(Self::toggle_replace) - }) - .on_action(Self::select_next_match) - .on_action(Self::select_prev_match) - .on_action(Self::cycle_mode) .w_full() .p_1() .child( @@ -292,7 +276,6 @@ impl ToolbarItemView for BufferSearchBar { return ToolbarItemLocation::Secondary; } } - ToolbarItemLocation::Hidden } @@ -334,22 +317,34 @@ impl BufferSearchBar { } register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { - this.toggle_case_sensitive(action, cx); + if this.supported_options().case { + this.toggle_case_sensitive(action, cx); + } }); register_action(workspace, |this, action: &ToggleWholeWord, cx| { - this.toggle_whole_word(action, cx); + if this.supported_options().word { + this.toggle_whole_word(action, cx); + } }); register_action(workspace, |this, action: &ToggleReplace, cx| { - this.toggle_replace(action, cx); + if this.supported_options().replacement { + this.toggle_replace(action, cx); + } }); register_action(workspace, |this, _: &ActivateRegexMode, cx| { - this.activate_search_mode(SearchMode::Regex, cx); + if this.supported_options().regex { + this.activate_search_mode(SearchMode::Regex, cx); + } }); register_action(workspace, |this, _: &ActivateTextMode, cx| { this.activate_search_mode(SearchMode::Text, cx); }); register_action(workspace, |this, action: &CycleMode, cx| { - this.cycle_mode(action, cx) + if this.supported_options().regex { + // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting + // cycling. + this.cycle_mode(action, cx) + } }); register_action(workspace, |this, action: &SelectNextMatch, cx| { this.select_next_match(action, cx); @@ -360,6 +355,11 @@ impl BufferSearchBar { register_action(workspace, |this, action: &SelectAllMatches, cx| { this.select_all_matches(action, cx); }); + register_action(workspace, |this, _: &editor::Cancel, cx| { + if !this.dismissed { + this.dismiss(&Dismiss, cx); + } + }); } pub fn new(cx: &mut ViewContext) -> Self { let query_editor = cx.build_view(|cx| Editor::single_line(cx)); @@ -393,7 +393,6 @@ impl BufferSearchBar { pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { self.dismissed = true; - for searchable_item in self.searchable_items_with_matches.keys() { if let Some(searchable_item) = WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) @@ -427,13 +426,18 @@ impl BufferSearchBar { if self.active_searchable_item.is_none() { return false; } - self.dismissed = false; cx.notify(); cx.emit(Event::UpdateLocation); true } + fn supported_options(&self) -> workspace::searchable::SearchOptions { + self.active_searchable_item + .as_deref() + .map(SearchableItemHandle::supported_options) + .unwrap_or_default() + } pub fn search_suggested(&mut self, cx: &mut ViewContext) { let search = self .query_suggestion(cx) @@ -542,16 +546,6 @@ impl BufferSearchBar { cx.notify(); } - fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext) { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if !search_bar.read(cx).dismissed { - search_bar.update(cx, |search_bar, cx| search_bar.dismiss(&Dismiss, cx)); - cx.stop_propagation(); - return; - } - } - } - pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { if let Some(active_editor) = self.active_searchable_item.as_ref() { let handle = active_editor.focus_handle(cx); @@ -810,28 +804,6 @@ impl BufferSearchBar { cx.notify(); } } - fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext) { - let mut should_propagate = true; - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| { - if let Some(_) = &bar.active_searchable_item { - should_propagate = false; - bar.replace_enabled = !bar.replace_enabled; - if bar.dismissed { - bar.show(cx); - } - if !bar.replace_enabled { - let handle = bar.query_editor.focus_handle(cx); - cx.focus(&handle); - } - cx.notify(); - } - }); - } - if !should_propagate { - cx.stop_propagation(); - } - } fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { let mut should_propagate = true; if !self.dismissed && self.active_search.is_some() { diff --git a/crates/search2/src/mode.rs b/crates/search2/src/mode.rs index bb729cb6c0..4b036d29a5 100644 --- a/crates/search2/src/mode.rs +++ b/crates/search2/src/mode.rs @@ -11,30 +11,6 @@ pub enum SearchMode { } impl SearchMode { - pub(crate) fn label(&self) -> &'static str { - match self { - SearchMode::Text => "Text", - SearchMode::Semantic => "Semantic", - SearchMode::Regex => "Regex", - } - } - - pub(crate) fn region_id(&self) -> usize { - match self { - SearchMode::Text => 3, - SearchMode::Semantic => 4, - SearchMode::Regex => 5, - } - } - - pub(crate) fn tooltip_text(&self) -> &'static str { - match self { - SearchMode::Text => "Activate Text Search", - SearchMode::Semantic => "Activate Semantic Search", - SearchMode::Regex => "Activate Regex Search", - } - } - pub(crate) fn activate_action(&self) -> Box { match self { SearchMode::Text => Box::new(ActivateTextMode), diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index 8879644eab..cd3b5e474b 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -35,35 +35,3 @@ pub(crate) fn render_search_mode_button( .on_click(Arc::new(on_click)) .variant(button_variant) } - -pub(crate) fn render_option_button_icon( - _is_active: bool, - _icon: &'static str, - _id: usize, - _label: impl Into>, - _action: Box, -) -> impl Component { - //let tooltip_style = cx.theme().tooltip.clone(); - div() - // MouseEventHandler::new::(id, cx, |state, cx| { - // let theme = cx.theme(); - // let style = theme - // .search - // .option_button - // .in_state(is_active) - // .style_for(state); - // Svg::new(icon) - // .with_color(style.color.clone()) - // .constrained() - // .with_width(style.icon_width) - // .contained() - // .with_style(style.container) - // .constrained() - // .with_height(theme.search.option_button_height) - // .with_width(style.button_width) - // }) - // .on_click(MouseButton::Left, on_click) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::(id, label, Some(action), tooltip_style, cx) - // .into_any() -} From ae1ebc68583f19cb23c679cb6f2dd62653b464a2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:22:52 +0100 Subject: [PATCH 014/205] fixup! Remove dead code (for now). --- crates/search2/src/mode.rs | 11 ++++------- crates/search2/src/search_bar.rs | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/search2/src/mode.rs b/crates/search2/src/mode.rs index 4b036d29a5..817fb454d2 100644 --- a/crates/search2/src/mode.rs +++ b/crates/search2/src/mode.rs @@ -1,6 +1,3 @@ -use gpui::Action; - -use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode}; // TODO: Update the default search mode to get from config #[derive(Copy, Clone, Debug, Default, PartialEq)] pub enum SearchMode { @@ -11,11 +8,11 @@ pub enum SearchMode { } impl SearchMode { - pub(crate) fn activate_action(&self) -> Box { + pub(crate) fn label(&self) -> &'static str { match self { - SearchMode::Text => Box::new(ActivateTextMode), - SearchMode::Semantic => Box::new(ActivateSemanticMode), - SearchMode::Regex => Box::new(ActivateRegexMode), + SearchMode::Text => "Text", + SearchMode::Semantic => "Semantic", + SearchMode::Regex => "Regex", } } } diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs index cd3b5e474b..1c4f2a17a6 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -1,6 +1,6 @@ -use std::{borrow::Cow, sync::Arc}; +use std::sync::Arc; -use gpui::{div, Action, Component, ViewContext}; +use gpui::{Component, ViewContext}; use ui::{Button, ButtonVariant, IconButton}; use crate::mode::SearchMode; From eb9959a0cf589f09bdf9bc43bf942e17c1b55199 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:23:05 +0100 Subject: [PATCH 015/205] gpui: notifications now takes an entity instead of a model --- crates/gpui2/src/app/test_context.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 940492573f..7b6acad586 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,9 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, - Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow, - View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, + BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, + KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, + TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle, + WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -296,21 +297,19 @@ impl TestAppContext { .unwrap() } - pub fn notifications(&mut self, entity: &Model) -> impl Stream { + pub fn notifications(&mut self, entity: &impl Entity) -> impl Stream { let (tx, rx) = futures::channel::mpsc::unbounded(); - - entity.update(self, move |_, cx: &mut ModelContext| { + self.update(|cx| { cx.observe(entity, { let tx = tx.clone(); - move |_, _, _| { + move |_, _| { let _ = tx.unbounded_send(()); } }) .detach(); - - cx.on_release(move |_, _| tx.close_channel()).detach(); + cx.observe_release(entity, move |_, _| tx.close_channel()) + .detach() }); - rx } From 3b5754a77e97f81987b30bfa420555f91ca696df Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 17 Nov 2023 17:23:47 +0100 Subject: [PATCH 016/205] Clean up tests (they compile now) --- crates/search2/src/buffer_search.rs | 470 ++++++++++++++-------------- 1 file changed, 241 insertions(+), 229 deletions(-) diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index d1f9103b67..1c1c882774 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -857,17 +857,33 @@ impl BufferSearchBar { mod tests { use super::*; use editor::{DisplayPoint, Editor}; - use gpui::{color::Color, test::EmptyView, TestAppContext}; + use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext}; use language::Buffer; + use smol::stream::StreamExt as _; use unindent::Unindent as _; - fn init_test(cx: &mut TestAppContext) -> (ViewHandle, ViewHandle) { - crate::project_search::tests::init_test(cx); - - let buffer = cx.add_model(|cx| { + fn init_globals(cx: &mut TestAppContext) { + cx.update(|cx| { + let store = settings::SettingsStore::test(cx); + cx.set_global(store); + editor::init(cx); + ui::init(cx); + language::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + }); + } + fn init_test( + cx: &mut TestAppContext, + ) -> ( + View, + View, + &mut VisualTestContext<'_>, + ) { + init_globals(cx); + let buffer = cx.build_model(|cx| { Buffer::new( 0, - cx.model_id() as u64, + cx.entity_id().as_u64(), r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -877,22 +893,22 @@ mod tests { .unindent(), ) }); - let window = cx.add_window(|_| EmptyView); - let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let (window, cx) = cx.add_window_view(|_| EmptyView {}); + let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); - let search_bar = window.add_view(cx, |cx| { + let search_bar = cx.build_view(|cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); search_bar }); - (editor, search_bar) + (editor, search_bar, cx) } #[gpui::test] async fn test_search_simple(cx: &mut TestAppContext) { - let (editor, search_bar) = init_test(cx); + let (editor, search_bar, cx) = init_test(cx); // Search for a string that appears with different casing. // By default, search is case-insensitive. @@ -906,11 +922,11 @@ mod tests { &[ ( DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), + Hsla::red(), ), ] ); @@ -920,13 +936,14 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); }); - editor.next_notification(cx).await; + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; editor.update(cx, |editor, cx| { assert_eq!( editor.all_text_background_highlights(cx), &[( DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), + Hsla::red(), )] ); }); @@ -943,31 +960,31 @@ mod tests { &[ ( DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), - Color::red(), + Hsla::red(), ), ] ); @@ -977,22 +994,23 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); }); - editor.next_notification(cx).await; + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; editor.update(cx, |editor, cx| { assert_eq!( editor.all_text_background_highlights(cx), &[ ( DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), - Color::red(), + Hsla::red(), ), ( DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), - Color::red(), + Hsla::red(), ), ] ); @@ -1011,7 +1029,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1022,7 +1040,7 @@ mod tests { [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(1)); }); @@ -1033,7 +1051,7 @@ mod tests { [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(2)); }); @@ -1044,7 +1062,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1055,7 +1073,7 @@ mod tests { [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(2)); }); @@ -1066,7 +1084,7 @@ mod tests { [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(1)); }); @@ -1077,7 +1095,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1096,7 +1114,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1115,7 +1133,7 @@ mod tests { [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(1)); }); @@ -1134,7 +1152,7 @@ mod tests { [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(2)); }); @@ -1153,7 +1171,7 @@ mod tests { [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(0)); }); @@ -1172,14 +1190,14 @@ mod tests { [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - search_bar.read_with(cx, |search_bar, _| { + search_bar.update(cx, |search_bar, _| { assert_eq!(search_bar.active_match_index, Some(2)); }); } #[gpui::test] async fn test_search_option_handling(cx: &mut TestAppContext) { - let (editor, search_bar) = init_test(cx); + let (editor, search_bar, cx) = init_test(cx); // show with options should make current search case sensitive search_bar @@ -1194,7 +1212,7 @@ mod tests { editor.all_text_background_highlights(cx), &[( DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), + Hsla::red(), )] ); }); @@ -1215,13 +1233,14 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) }); - editor.next_notification(cx).await; + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; editor.update(cx, |editor, cx| { assert_eq!( editor.all_text_background_highlights(cx), &[( DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), - Color::red(), + Hsla::red(), ),] ); }); @@ -1238,8 +1257,7 @@ mod tests { #[gpui::test] async fn test_search_select_all_matches(cx: &mut TestAppContext) { - crate::project_search::tests::init_test(cx); - + init_globals(cx); let buffer_text = r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -1255,186 +1273,180 @@ mod tests { expected_query_matches_count > 1, "Should pick a query with multiple results" ); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); - let window = cx.add_window(|_| EmptyView); - let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); + let window = cx.add_window(|_| EmptyView {}); - let search_bar = window.add_view(cx, |cx| { + let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = window.build_view(cx, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); search_bar }); - search_bar - .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) - .await - .unwrap(); - search_bar.update(cx, |search_bar, cx| { - cx.focus(search_bar.query_editor.as_any()); - search_bar.activate_current_match(cx); - }); - - window.read_with(cx, |cx| { - assert!( - !editor.is_focused(cx), - "Initially, the editor should not be focused" - ); - }); - - let initial_selections = editor.update(cx, |editor, cx| { - let initial_selections = editor.selections.display_ranges(cx); - assert_eq!( - initial_selections.len(), 1, - "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", - ); - initial_selections - }); - search_bar.update(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - search_bar.update(cx, |search_bar, cx| { - cx.focus(search_bar.query_editor.as_any()); - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - window.read_with(cx, |cx| { - assert!( - editor.is_focused(cx), - "Should focus editor after successful SelectAllMatches" - ); - }); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should not change after selecting all matches" - ); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_next_match(&SelectNextMatch, cx); - }); - window.read_with(cx, |cx| { - assert!( - editor.is_focused(cx), - "Should still have editor focused after SelectNextMatch" - ); - }); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - 1, - "On next match, should deselect items and select the next match" - ); - assert_ne!( - all_selections, initial_selections, - "Next match should be different from the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should be updated to the next one" - ); - }); - - search_bar.update(cx, |search_bar, cx| { - cx.focus(search_bar.query_editor.as_any()); - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - window.read_with(cx, |cx| { - assert!( - editor.is_focused(cx), - "Should focus editor after successful SelectAllMatches" - ); - }); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should not change after selecting all matches" - ); - }); - - search_bar.update(cx, |search_bar, cx| { - search_bar.select_prev_match(&SelectPrevMatch, cx); - }); - window.read_with(cx, |cx| { - assert!( - editor.is_focused(cx), - "Should still have editor focused after SelectPrevMatch" - ); - }); - let last_match_selections = search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections.len(), - 1, - "On previous match, should deselect items and select the previous item" - ); - assert_eq!( - all_selections, initial_selections, - "Previous match should be the same as the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should be updated to the previous one" - ); - all_selections - }); - - search_bar - .update(cx, |search_bar, cx| { - cx.focus(search_bar.query_editor.as_any()); - search_bar.search("abas_nonexistent_match", None, cx) + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx)) }) + .unwrap() .await .unwrap(); - search_bar.update(cx, |search_bar, cx| { - search_bar.select_all_matches(&SelectAllMatches, cx); - }); - window.read_with(cx, |cx| { + + let last_match_selections = window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.activate_current_match(cx); + }); + assert!( + !editor.read(cx).is_focused(cx), + "Initially, the editor should not be focused" + ); + let initial_selections = editor.update(cx, |editor, cx| { + let initial_selections = editor.selections.display_ranges(cx); + assert_eq!( + initial_selections.len(), 1, + "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", + ); + initial_selections + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + assert!( + editor.read(cx).is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should not change after selecting all matches" + ); + }); + search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx)); + assert!( + editor.read(cx).is_focused(cx), + "Should still have editor focused after SelectNextMatch" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On next match, should deselect items and select the next match" + ); + assert_ne!( + all_selections, initial_selections, + "Next match should be different from the first selection" + ); + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should be updated to the next one" + ); + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + assert!( + editor.read(cx).is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should not change after selecting all matches" + ); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + }); + assert!( + editor.read(cx).is_focused(&cx), + "Should still have editor focused after SelectPrevMatch" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On previous match, should deselect items and select the previous item" + ); + assert_eq!( + all_selections, initial_selections, + "Previous match should be the same as the first selection" + ); + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should be updated to the previous one" + ); + all_selections + }) + }) + .unwrap(); + + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.search("abas_nonexistent_match", None, cx) + }) + }) + .unwrap() + .await + .unwrap(); + window.update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); assert!( - !editor.is_focused(cx), + editor.update(cx, |this, cx| !this.is_focused(cx.window_context())), "Should not switch focus to editor if SelectAllMatches does not find any matches" ); - }); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); - assert_eq!( - all_selections, last_match_selections, - "Should not select anything new if there are no matches" - ); - assert!( - search_bar.active_match_index.is_none(), - "For no matches, there should be no active match index" - ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections, last_match_selections, + "Should not select anything new if there are no matches" + ); + assert!( + search_bar.active_match_index.is_none(), + "For no matches, there should be no active match index" + ); + }); }); } #[gpui::test] async fn test_search_query_history(cx: &mut TestAppContext) { - crate::project_search::tests::init_test(cx); - + //crate::project_search::tests::init_test(cx); + init_globals(cx); let buffer_text = r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search @@ -1442,12 +1454,12 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); - let window = cx.add_window(|_| EmptyView); + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); + let (window, cx) = cx.add_window_view(|_| EmptyView {}); - let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); - let search_bar = window.add_view(cx, |cx| { + let search_bar = cx.build_view(|cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); @@ -1470,7 +1482,7 @@ mod tests { .await .unwrap(); // Ensure that the latest search is active. - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "c"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1479,14 +1491,14 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), ""); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), ""); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1495,7 +1507,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "c"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1504,7 +1516,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "b"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1513,14 +1525,14 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "a"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "a"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1529,7 +1541,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "b"); assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); }); @@ -1538,7 +1550,7 @@ mod tests { .update(cx, |search_bar, cx| search_bar.search("ba", None, cx)) .await .unwrap(); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "ba"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); @@ -1547,42 +1559,42 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "c"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); search_bar.update(cx, |search_bar, cx| { search_bar.previous_history_query(&PreviousHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "b"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "c"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), "ba"); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); search_bar.update(cx, |search_bar, cx| { search_bar.next_history_query(&NextHistoryQuery, cx); }); - search_bar.read_with(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { assert_eq!(search_bar.query(cx), ""); assert_eq!(search_bar.search_options, SearchOptions::NONE); }); } #[gpui::test] async fn test_replace_simple(cx: &mut TestAppContext) { - let (editor, search_bar) = init_test(cx); + let (editor, search_bar, cx) = init_test(cx); search_bar .update(cx, |search_bar, cx| { @@ -1599,7 +1611,7 @@ mod tests { search_bar.replace_all(&ReplaceAll, cx) }); assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), + editor.update(cx, |this, cx| { this.text(cx) }), r#" A regular expr$1 (shortened as regex or regexp;[1] also referred to as rational expr$1[2][3]) is a sequence of characters that specifies a search @@ -1625,7 +1637,7 @@ mod tests { }); // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), + editor.update(cx, |this, cx| { this.text(cx) }), r#" A regular expr$1 (shortened as regex banana regexp;[1] also referred to as rational expr$1[2][3]) is a sequence of characters that specifies a search @@ -1649,7 +1661,7 @@ mod tests { search_bar.replace_all(&ReplaceAll, cx) }); assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), + editor.update(cx, |this, cx| { this.text(cx) }), r#" A regular expr$1 (shortened as regex banana regexp;1number also referred to as rational expr$12number3number) is a sequence of characters that specifies a search @@ -1675,7 +1687,7 @@ mod tests { // The only word affected by this edit should be `algorithms`, even though there's a bunch // of words in this text that would match this regex if not for WHOLE_WORD. assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), + editor.update(cx, |this, cx| { this.text(cx) }), r#" A regular expr$1 (shortened as regex banana regexp;1number also referred to as rational expr$12number3number) is a sequence of characters that specifies a search From c866c211b53c7bb2bfd96af69f2144073aa8f101 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Nov 2023 13:48:01 -0700 Subject: [PATCH 017/205] Make static str and SharedString implement Element --- crates/gpui2/src/elements/text.rs | 138 +++++++++++++++++++++++++++++- 1 file changed, 136 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 6849a89711..9e8256014e 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,12 +1,72 @@ use crate::{ AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels, - SharedString, Size, TextRun, ViewContext, WrappedLine, + SharedString, Size, TextRun, ViewContext, WindowContext, WrappedLine, }; +use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; use std::{cell::Cell, rc::Rc, sync::Arc}; use util::ResultExt; +impl Element for &'static str { + type ElementState = TextState; + + fn element_id(&self) -> Option { + None + } + + fn layout( + &mut self, + _: &mut V, + _: Option, + cx: &mut ViewContext, + ) -> (LayoutId, Self::ElementState) { + let mut state = TextState::default(); + let layout_id = state.layout(SharedString::from(*self), None, cx); + (layout_id, state) + } + + fn paint( + &mut self, + bounds: Bounds, + _: &mut V, + state: &mut TextState, + cx: &mut ViewContext, + ) { + state.paint(bounds, self, cx) + } +} + +impl Element for SharedString { + type ElementState = TextState; + + fn element_id(&self) -> Option { + Some(self.clone().into()) + } + + fn layout( + &mut self, + _: &mut V, + _: Option, + cx: &mut ViewContext, + ) -> (LayoutId, Self::ElementState) { + let mut state = TextState::default(); + let layout_id = state.layout(self.clone(), None, cx); + (layout_id, state) + } + + fn paint( + &mut self, + bounds: Bounds, + _: &mut V, + state: &mut TextState, + cx: &mut ViewContext, + ) { + let text_str: &str = self.as_ref(); + state.paint(bounds, text_str, cx) + } +} + pub struct Text { text: SharedString, runs: Option>, @@ -109,7 +169,7 @@ impl Element for Text { let element_state = element_state.lock(); let element_state = element_state .as_ref() - .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text)) + .ok_or_else(|| anyhow!("measurement has not been performed on {}", &self.text)) .unwrap(); let line_height = element_state.line_height; @@ -128,6 +188,80 @@ impl TextState { fn lock(&self) -> MutexGuard> { self.0.lock() } + + fn layout( + &mut self, + text: SharedString, + runs: Option>, + cx: &mut WindowContext, + ) -> LayoutId { + let text_system = cx.text_system().clone(); + let text_style = cx.text_style(); + let font_size = text_style.font_size.to_pixels(cx.rem_size()); + let line_height = text_style + .line_height + .to_pixels(font_size.into(), cx.rem_size()); + let text = SharedString::from(text); + + let rem_size = cx.rem_size(); + + let runs = if let Some(runs) = runs { + runs + } else { + vec![text_style.to_run(text.len())] + }; + + let layout_id = cx.request_measured_layout(Default::default(), rem_size, { + let element_state = self.clone(); + move |known_dimensions, _| { + let Some(lines) = text_system + .shape_text( + &text, + font_size, + &runs[..], + known_dimensions.width, // Wrap if we know the width. + ) + .log_err() + else { + element_state.lock().replace(TextStateInner { + lines: Default::default(), + line_height, + }); + return Size::default(); + }; + + let mut size: Size = Size::default(); + for line in &lines { + let line_size = line.size(line_height); + size.height += line_size.height; + size.width = size.width.max(line_size.width); + } + + element_state + .lock() + .replace(TextStateInner { lines, line_height }); + + size + } + }); + + layout_id + } + + fn paint(&mut self, bounds: Bounds, text: &str, cx: &mut WindowContext) { + let element_state = self.lock(); + let element_state = element_state + .as_ref() + .ok_or_else(|| anyhow!("measurement has not been performed on {}", text)) + .unwrap(); + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + for line in &element_state.lines { + line.paint(line_origin, line_height, cx).log_err(); + line_origin.y += line.size(line_height).height; + } + } } struct TextStateInner { From 0069dd5ce67981428d4ceae3b191b9083725a30b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Nov 2023 20:05:37 -0700 Subject: [PATCH 018/205] WIP --- crates/gpui2/src/element.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index b4b1af630e..26232620d9 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -5,7 +5,7 @@ use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; use std::{any::Any, fmt::Debug, mem}; -pub trait Element { +pub trait Element: 'static + Sized { type ElementState: 'static; fn element_id(&self) -> Option; @@ -25,6 +25,10 @@ pub trait Element { cx: &mut ViewContext, ); + fn into_any(self) -> AnyElement { + AnyElement::new(self) + } + fn draw( self, origin: Point, @@ -34,7 +38,6 @@ pub trait Element { f: impl FnOnce(&Self::ElementState, &mut ViewContext) -> R, ) -> R where - Self: Sized, T: Clone + Default + Debug + Into, { let mut element = RenderedElement { From 2515bbf990096839dbaffe338d1ae65f6ca02c80 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 17 Nov 2023 23:32:55 -0700 Subject: [PATCH 019/205] Move self in Element::paint Remove mutable state borrows in favor of state ownership in render processes to streamline element rendering. --- crates/editor2/src/element.rs | 20 +- crates/gpui2/src/app.rs | 10 +- crates/gpui2/src/app/entity_map.rs | 10 +- crates/gpui2/src/element.rs | 219 +++++++++++++--------- crates/gpui2/src/elements/div.rs | 58 +++--- crates/gpui2/src/elements/img.rs | 10 +- crates/gpui2/src/elements/overlay.rs | 12 +- crates/gpui2/src/elements/svg.rs | 10 +- crates/gpui2/src/elements/text.rs | 36 ++-- crates/gpui2/src/elements/uniform_list.rs | 27 ++- crates/gpui2/src/gpui2.rs | 2 - crates/gpui2/src/view.rs | 53 +++--- crates/gpui2/src/window.rs | 4 +- crates/gpui2_macros/src/gpui2_macros.rs | 6 + crates/ui2/src/components/context_menu.rs | 16 +- 15 files changed, 262 insertions(+), 231 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3de5389b1f..2de9182901 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -488,8 +488,8 @@ impl EditorElement { } } - for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() { - if let Some(fold_indicator) = fold_indicator.as_mut() { + for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { + if let Some(mut fold_indicator) = fold_indicator { let available_space = size( AvailableSpace::MinContent, AvailableSpace::Definite(line_height * 0.55), @@ -509,7 +509,7 @@ impl EditorElement { } } - if let Some(indicator) = layout.code_actions_indicator.as_mut() { + if let Some(mut indicator) = layout.code_actions_indicator.take() { let available_space = size( AvailableSpace::MinContent, AvailableSpace::Definite(line_height), @@ -840,7 +840,7 @@ impl EditorElement { } }); - if let Some((position, context_menu)) = layout.context_menu.as_mut() { + if let Some((position, mut context_menu)) = layout.context_menu.take() { cx.with_z_index(1, |cx| { let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); let available_space = size( @@ -1224,7 +1224,7 @@ impl EditorElement { let scroll_left = scroll_position.x * layout.position_map.em_width; let scroll_top = scroll_position.y * layout.position_map.line_height; - for block in &mut layout.blocks { + for block in layout.blocks.drain(..) { let mut origin = bounds.origin + point( Pixels::ZERO, @@ -2389,7 +2389,7 @@ enum Invisible { } impl Element for EditorElement { - type ElementState = (); + type State = (); fn element_id(&self) -> Option { Some(self.editor_id.into()) @@ -2398,9 +2398,9 @@ impl Element for EditorElement { fn layout( &mut self, editor: &mut Editor, - element_state: Option, + element_state: Option, cx: &mut gpui::ViewContext, - ) -> (gpui::LayoutId, Self::ElementState) { + ) -> (gpui::LayoutId, Self::State) { editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. let rem_size = cx.rem_size(); @@ -2416,10 +2416,10 @@ impl Element for EditorElement { } fn paint( - &mut self, + mut self, bounds: Bounds, editor: &mut Editor, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut gpui::ViewContext, ) { let mut layout = self.compute_layout(editor, cx, bounds); diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index b5083b97c2..397b770c2b 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -14,7 +14,7 @@ use smallvec::SmallVec; pub use test_context::*; use crate::{ - current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView, + current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform, @@ -28,7 +28,7 @@ use futures::{channel::oneshot, future::LocalBoxFuture, Future}; use parking_lot::Mutex; use slotmap::SlotMap; use std::{ - any::{type_name, Any, TypeId}, + any::{type_name, TypeId}, cell::{Ref, RefCell, RefMut}, marker::PhantomData, mem, @@ -194,7 +194,7 @@ pub struct AppContext { asset_source: Arc, pub(crate) image_cache: ImageCache, pub(crate) text_style_stack: Vec, - pub(crate) globals_by_type: HashMap, + pub(crate) globals_by_type: HashMap>, pub(crate) entities: EntityMap, pub(crate) new_view_observers: SubscriberSet, pub(crate) windows: SlotMap>, @@ -1100,12 +1100,12 @@ pub(crate) enum Effect { /// Wraps a global variable value during `update_global` while the value has been moved to the stack. pub(crate) struct GlobalLease { - global: AnyBox, + global: Box, global_type: PhantomData, } impl GlobalLease { - fn new(global: AnyBox) -> Self { + fn new(global: Box) -> Self { GlobalLease { global, global_type: PhantomData, diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index 4a3cca040b..e909e50b10 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -1,10 +1,10 @@ -use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext}; +use crate::{private::Sealed, AppContext, Context, Entity, ModelContext}; use anyhow::{anyhow, Result}; use derive_more::{Deref, DerefMut}; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use slotmap::{SecondaryMap, SlotMap}; use std::{ - any::{type_name, TypeId}, + any::{type_name, Any, TypeId}, fmt::{self, Display}, hash::{Hash, Hasher}, marker::PhantomData, @@ -31,7 +31,7 @@ impl Display for EntityId { } pub(crate) struct EntityMap { - entities: SecondaryMap, + entities: SecondaryMap>, ref_counts: Arc>, } @@ -101,7 +101,7 @@ impl EntityMap { ); } - pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> { + pub fn take_dropped(&mut self) -> Vec<(EntityId, Box)> { let mut ref_counts = self.ref_counts.write(); let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids); @@ -121,7 +121,7 @@ impl EntityMap { } pub struct Lease<'a, T> { - entity: Option, + entity: Option>, pub model: &'a Model, entity_type: PhantomData, } diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 26232620d9..b9d312755a 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -3,25 +3,25 @@ use crate::{ }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; -use std::{any::Any, fmt::Debug, mem}; +use std::{any::Any, fmt::Debug}; pub trait Element: 'static + Sized { - type ElementState: 'static; + type State: 'static; fn element_id(&self) -> Option; fn layout( &mut self, view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState); + ) -> (LayoutId, Self::State); fn paint( - &mut self, + self, bounds: Bounds, view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ); @@ -35,32 +35,26 @@ pub trait Element: 'static + Sized { available_space: Size, view_state: &mut V, cx: &mut ViewContext, - f: impl FnOnce(&Self::ElementState, &mut ViewContext) -> R, + f: impl FnOnce(&mut Self::State, &mut ViewContext) -> R, ) -> R where T: Clone + Default + Debug + Into, { - let mut element = RenderedElement { - element: self, - phase: ElementRenderPhase::Start, + let element_id = self.element_id(); + let element = DrawableElement { + element: Some(self), + phase: ElementDrawPhase::Start, }; - element.draw(origin, available_space.map(Into::into), view_state, cx); - if let ElementRenderPhase::Painted { frame_state } = &element.phase { - if let Some(frame_state) = frame_state.as_ref() { - f(&frame_state, cx) - } else { - let element_id = element - .element - .element_id() - .expect("we either have some frame_state or some element_id"); - cx.with_element_state(element_id, |element_state, cx| { - let element_state = element_state.unwrap(); - let result = f(&element_state, cx); - (result, element_state) - }) - } + let frame_state = element.draw(origin, available_space.map(Into::into), view_state, cx); + + if let Some(mut frame_state) = frame_state { + f(&mut frame_state, cx) } else { - unreachable!() + cx.with_element_state(element_id.unwrap(), |element_state, cx| { + let mut element_state = element_state.unwrap(); + let result = f(&mut element_state, cx); + (result, element_state) + }) } } } @@ -107,13 +101,13 @@ trait ElementObject { ); } -struct RenderedElement> { - element: E, - phase: ElementRenderPhase, +pub struct DrawableElement> { + element: Option, + phase: ElementDrawPhase, } #[derive(Default)] -enum ElementRenderPhase { +enum ElementDrawPhase { #[default] Start, LayoutRequested { @@ -125,83 +119,83 @@ enum ElementRenderPhase { available_space: Size, frame_state: Option, }, - Painted { - frame_state: Option, - }, } /// Internal struct that wraps an element to store Layout and ElementState after the element is rendered. /// It's allocated as a trait object to erase the element type and wrapped in AnyElement for /// improved usability. -impl> RenderedElement { +impl> DrawableElement { fn new(element: E) -> Self { - RenderedElement { - element, - phase: ElementRenderPhase::Start, + DrawableElement { + element: Some(element), + phase: ElementDrawPhase::Start, } } -} -impl ElementObject for RenderedElement -where - E: Element, - E::ElementState: 'static, -{ fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { - let (layout_id, frame_state) = match mem::take(&mut self.phase) { - ElementRenderPhase::Start => { - if let Some(id) = self.element.element_id() { - let layout_id = cx.with_element_state(id, |element_state, cx| { - self.element.layout(state, element_state, cx) - }); - (layout_id, None) - } else { - let (layout_id, frame_state) = self.element.layout(state, None, cx); - (layout_id, Some(frame_state)) - } - } - ElementRenderPhase::LayoutRequested { .. } - | ElementRenderPhase::LayoutComputed { .. } - | ElementRenderPhase::Painted { .. } => { - panic!("element rendered twice") - } + let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id() + { + let layout_id = cx.with_element_state(id, |element_state, cx| { + self.element + .as_mut() + .unwrap() + .layout(state, element_state, cx) + }); + (layout_id, None) + } else { + let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(state, None, cx); + (layout_id, Some(frame_state)) }; - self.phase = ElementRenderPhase::LayoutRequested { + self.phase = ElementDrawPhase::LayoutRequested { layout_id, frame_state, }; layout_id } - fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { - self.phase = match mem::take(&mut self.phase) { - ElementRenderPhase::LayoutRequested { + fn paint(mut self, view_state: &mut V, cx: &mut ViewContext) -> Option { + match self.phase { + ElementDrawPhase::LayoutRequested { layout_id, - mut frame_state, + frame_state, } - | ElementRenderPhase::LayoutComputed { + | ElementDrawPhase::LayoutComputed { layout_id, - mut frame_state, + frame_state, .. } => { let bounds = cx.layout_bounds(layout_id); - if let Some(id) = self.element.element_id() { - cx.with_element_state(id, |element_state, cx| { + + if let Some(mut frame_state) = frame_state { + self.element + .take() + .unwrap() + .paint(bounds, view_state, &mut frame_state, cx); + Some(frame_state) + } else { + let element_id = self + .element + .as_ref() + .unwrap() + .element_id() + .expect("if we don't have frame state, we should have element state"); + cx.with_element_state(element_id, |element_state, cx| { let mut element_state = element_state.unwrap(); - self.element - .paint(bounds, view_state, &mut element_state, cx); + self.element.take().unwrap().paint( + bounds, + view_state, + &mut element_state, + cx, + ); ((), element_state) }); - } else { - self.element - .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx); + None } - ElementRenderPhase::Painted { frame_state } } _ => panic!("must call layout before paint"), - }; + } } fn measure( @@ -210,25 +204,25 @@ where view_state: &mut V, cx: &mut ViewContext, ) -> Size { - if matches!(&self.phase, ElementRenderPhase::Start) { + if matches!(&self.phase, ElementDrawPhase::Start) { self.layout(view_state, cx); } let layout_id = match &mut self.phase { - ElementRenderPhase::LayoutRequested { + ElementDrawPhase::LayoutRequested { layout_id, frame_state, } => { cx.compute_layout(*layout_id, available_space); let layout_id = *layout_id; - self.phase = ElementRenderPhase::LayoutComputed { + self.phase = ElementDrawPhase::LayoutComputed { layout_id, available_space, frame_state: frame_state.take(), }; layout_id } - ElementRenderPhase::LayoutComputed { + ElementDrawPhase::LayoutComputed { layout_id, available_space: prev_available_space, .. @@ -245,6 +239,40 @@ where cx.layout_bounds(layout_id).size } + fn draw( + mut self, + origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) -> Option { + self.measure(available_space, view_state, cx); + cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx)) + } +} + +impl ElementObject for Option> +where + E: Element, + E::State: 'static, +{ + fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { + DrawableElement::layout(self.as_mut().unwrap(), view_state, cx) + } + + fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { + DrawableElement::paint(self.take().unwrap(), view_state, cx); + } + + fn measure( + &mut self, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + ) -> Size { + DrawableElement::measure(self.as_mut().unwrap(), available_space, view_state, cx) + } + fn draw( &mut self, origin: Point, @@ -252,8 +280,13 @@ where view_state: &mut V, cx: &mut ViewContext, ) { - self.measure(available_space, view_state, cx); - cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx)) + DrawableElement::draw( + self.take().unwrap(), + origin, + available_space, + view_state, + cx, + ); } } @@ -264,16 +297,16 @@ impl AnyElement { where V: 'static, E: 'static + Element, - E::ElementState: Any, + E::State: Any, { - AnyElement(Box::new(RenderedElement::new(element))) + AnyElement(Box::new(Some(DrawableElement::new(element))) as Box>) } pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { self.0.layout(view_state, cx) } - pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { + pub fn paint(mut self, view_state: &mut V, cx: &mut ViewContext) { self.0.paint(view_state, cx) } @@ -289,7 +322,7 @@ impl AnyElement { /// Initializes this element and performs layout in the available space, then paints it at the given origin. pub fn draw( - &mut self, + mut self, origin: Point, available_space: Size, view_state: &mut V, @@ -343,7 +376,7 @@ where E: 'static + Component, F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, { - type ElementState = AnyElement; + type State = Option>; fn element_id(&self) -> Option { None @@ -352,23 +385,23 @@ where fn layout( &mut self, view_state: &mut V, - _: Option, + _: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { let render = self.take().unwrap(); let mut rendered_element = (render)(view_state, cx).render(); let layout_id = rendered_element.layout(view_state, cx); - (layout_id, rendered_element) + (layout_id, Some(rendered_element)) } fn paint( - &mut self, + self, _bounds: Bounds, view_state: &mut V, - rendered_element: &mut Self::ElementState, + rendered_element: &mut Self::State, cx: &mut ViewContext, ) { - rendered_element.paint(view_state, cx) + rendered_element.take().unwrap().paint(view_state, cx); } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index f9560f2c53..1a70a9bab2 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -600,7 +600,7 @@ impl ParentComponent for Div { } impl Element for Div { - type ElementState = DivState; + type State = DivState; fn element_id(&self) -> Option { self.interactivity.element_id.clone() @@ -609,9 +609,9 @@ impl Element for Div { fn layout( &mut self, view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { let mut child_layout_ids = SmallVec::new(); let mut interactivity = mem::take(&mut self.interactivity); let (layout_id, interactive_state) = interactivity.layout( @@ -639,10 +639,10 @@ impl Element for Div { } fn paint( - &mut self, + self, bounds: Bounds, view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ) { let mut child_min = point(Pixels::MAX, Pixels::MAX); @@ -658,8 +658,7 @@ impl Element for Div { (child_max - child_min).into() }; - let mut interactivity = mem::take(&mut self.interactivity); - interactivity.paint( + self.interactivity.paint( bounds, content_size, &mut element_state.interactive_state, @@ -679,7 +678,7 @@ impl Element for Div { cx.with_text_style(style.text_style().cloned(), |cx| { cx.with_content_mask(style.overflow_mask(bounds), |cx| { cx.with_element_offset(scroll_offset, |cx| { - for child in &mut self.children { + for child in self.children { child.paint(view_state, cx); } }) @@ -689,7 +688,6 @@ impl Element for Div { }) }, ); - self.interactivity = interactivity; } } @@ -770,7 +768,7 @@ where } pub fn paint( - &mut self, + mut self, bounds: Bounds, content_size: Size, element_state: &mut InteractiveElementState, @@ -786,25 +784,25 @@ where } } - for listener in self.mouse_down_listeners.drain(..) { + for listener in self.mouse_down_listeners { cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { listener(state, event, &bounds, phase, cx); }) } - for listener in self.mouse_up_listeners.drain(..) { + for listener in self.mouse_up_listeners { cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { listener(state, event, &bounds, phase, cx); }) } - for listener in self.mouse_move_listeners.drain(..) { + for listener in self.mouse_move_listeners { cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| { listener(state, event, &bounds, phase, cx); }) } - for listener in self.scroll_wheel_listeners.drain(..) { + for listener in self.scroll_wheel_listeners { cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { listener(state, event, &bounds, phase, cx); }) @@ -840,7 +838,7 @@ where } if cx.active_drag.is_some() { - let drop_listeners = mem::take(&mut self.drop_listeners); + let drop_listeners = self.drop_listeners; cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { if let Some(drag_state_type) = @@ -1062,24 +1060,24 @@ where self.key_context.clone(), element_state.focus_handle.clone(), |_, cx| { - for listener in self.key_down_listeners.drain(..) { + for listener in self.key_down_listeners { cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { listener(state, event, phase, cx); }) } - for listener in self.key_up_listeners.drain(..) { + for listener in self.key_up_listeners { cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { listener(state, event, phase, cx); }) } - for (action_type, listener) in self.action_listeners.drain(..) { + for (action_type, listener) in self.action_listeners { cx.on_action(action_type, listener) } if let Some(focus_handle) = element_state.focus_handle.as_ref() { - for listener in self.focus_listeners.drain(..) { + for listener in self.focus_listeners { let focus_handle = focus_handle.clone(); cx.on_focus_changed(move |view, event, cx| { listener(view, &focus_handle, event, cx) @@ -1291,7 +1289,7 @@ where V: 'static, E: Element, { - type ElementState = E::ElementState; + type State = E::State; fn element_id(&self) -> Option { self.element.element_id() @@ -1300,20 +1298,20 @@ where fn layout( &mut self, view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { self.element.layout(view_state, element_state, cx) } fn paint( - &mut self, + self, bounds: Bounds, view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ) { - self.element.paint(bounds, view_state, element_state, cx); + self.element.paint(bounds, view_state, element_state, cx) } } @@ -1377,7 +1375,7 @@ where V: 'static, E: Element, { - type ElementState = E::ElementState; + type State = E::State; fn element_id(&self) -> Option { self.element.element_id() @@ -1386,17 +1384,17 @@ where fn layout( &mut self, view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { self.element.layout(view_state, element_state, cx) } fn paint( - &mut self, + self, bounds: Bounds, view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ) { self.element.paint(bounds, view_state, element_state, cx) diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 1080135fe1..db55bd0dc6 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -42,7 +42,7 @@ impl Component for Img { } impl Element for Img { - type ElementState = InteractiveElementState; + type State = InteractiveElementState; fn element_id(&self) -> Option { self.interactivity.element_id.clone() @@ -51,19 +51,19 @@ impl Element for Img { fn layout( &mut self, _view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) } fn paint( - &mut self, + self, bounds: Bounds, _view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ) { self.interactivity.paint( diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 14a8048d39..394030f3d7 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -64,7 +64,7 @@ impl Component for Overlay { } impl Element for Overlay { - type ElementState = OverlayState; + type State = OverlayState; fn element_id(&self) -> Option { None @@ -73,9 +73,9 @@ impl Element for Overlay { fn layout( &mut self, view_state: &mut V, - _: Option, + _: Option, cx: &mut crate::ViewContext, - ) -> (crate::LayoutId, Self::ElementState) { + ) -> (crate::LayoutId, Self::State) { let child_layout_ids = self .children .iter_mut() @@ -92,10 +92,10 @@ impl Element for Overlay { } fn paint( - &mut self, + self, bounds: crate::Bounds, view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut crate::ViewContext, ) { if element_state.child_layout_ids.is_empty() { @@ -156,7 +156,7 @@ impl Element for Overlay { } cx.with_element_offset(desired.origin - bounds.origin, |cx| { - for child in &mut self.children { + for child in self.children { child.paint(view_state, cx); } }) diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index c1c7691fbf..7d9c04b729 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -31,7 +31,7 @@ impl Component for Svg { } impl Element for Svg { - type ElementState = InteractiveElementState; + type State = InteractiveElementState; fn element_id(&self) -> Option { self.interactivity.element_id.clone() @@ -40,19 +40,19 @@ impl Element for Svg { fn layout( &mut self, _view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) } fn paint( - &mut self, + self, bounds: Bounds, _view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ) where Self: Sized, diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 9e8256014e..c6090fdfb4 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -9,7 +9,7 @@ use std::{cell::Cell, rc::Rc, sync::Arc}; use util::ResultExt; impl Element for &'static str { - type ElementState = TextState; + type State = TextState; fn element_id(&self) -> Option { None @@ -18,16 +18,16 @@ impl Element for &'static str { fn layout( &mut self, _: &mut V, - _: Option, + _: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { let mut state = TextState::default(); let layout_id = state.layout(SharedString::from(*self), None, cx); (layout_id, state) } fn paint( - &mut self, + self, bounds: Bounds, _: &mut V, state: &mut TextState, @@ -38,7 +38,7 @@ impl Element for &'static str { } impl Element for SharedString { - type ElementState = TextState; + type State = TextState; fn element_id(&self) -> Option { Some(self.clone().into()) @@ -47,16 +47,16 @@ impl Element for SharedString { fn layout( &mut self, _: &mut V, - _: Option, + _: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { let mut state = TextState::default(); let layout_id = state.layout(self.clone(), None, cx); (layout_id, state) } fn paint( - &mut self, + self, bounds: Bounds, _: &mut V, state: &mut TextState, @@ -93,7 +93,7 @@ impl Component for Text { } impl Element for Text { - type ElementState = TextState; + type State = TextState; fn element_id(&self) -> Option { None @@ -102,9 +102,9 @@ impl Element for Text { fn layout( &mut self, _view: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { let element_state = element_state.unwrap_or_default(); let text_system = cx.text_system().clone(); let text_style = cx.text_style(); @@ -160,10 +160,10 @@ impl Element for Text { } fn paint( - &mut self, + self, bounds: Bounds, _: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ) { let element_state = element_state.lock(); @@ -280,7 +280,7 @@ struct InteractiveTextState { } impl Element for InteractiveText { - type ElementState = InteractiveTextState; + type State = InteractiveTextState; fn element_id(&self) -> Option { Some(self.id.clone()) @@ -289,9 +289,9 @@ impl Element for InteractiveText { fn layout( &mut self, view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { if let Some(InteractiveTextState { text_state, clicked_range_ixs, @@ -314,10 +314,10 @@ impl Element for InteractiveText { } fn paint( - &mut self, + self, bounds: Bounds, view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ) { self.text diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 773f9ec8aa..d7339d00ab 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -4,7 +4,7 @@ use crate::{ Point, Size, StyleRefinement, Styled, ViewContext, }; use smallvec::SmallVec; -use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc}; +use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; use taffy::style::Overflow; /// uniform_list provides lazy rendering for a set of items that are of uniform height. @@ -102,7 +102,7 @@ pub struct UniformListState { } impl Element for UniformList { - type ElementState = UniformListState; + type State = UniformListState; fn element_id(&self) -> Option { Some(self.id.clone()) @@ -111,9 +111,9 @@ impl Element for UniformList { fn layout( &mut self, view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { let max_items = self.item_count; let rem_size = cx.rem_size(); let item_size = element_state @@ -159,10 +159,10 @@ impl Element for UniformList { } fn paint( - &mut self, + self, bounds: Bounds, view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut ViewContext, ) { let style = @@ -183,14 +183,17 @@ impl Element for UniformList { height: item_size.height * self.item_count, }; - let mut interactivity = mem::take(&mut self.interactivity); let shared_scroll_offset = element_state .interactive .scroll_offset .get_or_insert_with(Rc::default) .clone(); - interactivity.paint( + let item_height = self + .measure_item(view_state, Some(padded_bounds.size.width), cx) + .height; + + self.interactivity.paint( bounds, content_size, &mut element_state.interactive, @@ -209,9 +212,6 @@ impl Element for UniformList { style.paint(bounds, cx); if self.item_count > 0 { - let item_height = self - .measure_item(view_state, Some(padded_bounds.size.width), cx) - .height; if let Some(scroll_handle) = self.scroll_handle.clone() { scroll_handle.0.borrow_mut().replace(ScrollHandleState { item_height, @@ -233,9 +233,9 @@ impl Element for UniformList { self.item_count, ); - let mut items = (self.render_items)(view_state, visible_range.clone(), cx); + let items = (self.render_items)(view_state, visible_range.clone(), cx); cx.with_z_index(1, |cx| { - for (item, ix) in items.iter_mut().zip(visible_range) { + for (item, ix) in items.into_iter().zip(visible_range) { let item_origin = padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset.y); let available_space = size( @@ -249,7 +249,6 @@ impl Element for UniformList { }) }, ); - self.interactivity = interactivity; } } diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 88ecd52c03..4423986e3d 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -78,8 +78,6 @@ use std::{ }; use taffy::TaffyLayoutEngine; -type AnyBox = Box; - pub trait Context { type Result; diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 6b8c8a3eef..51675e814c 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,8 +1,7 @@ use crate::{ - private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, - BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, - FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel, - WindowContext, + private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, + Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, + LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{ @@ -164,7 +163,7 @@ impl Eq for WeakView {} pub struct AnyView { model: AnyModel, layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), - paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), + paint: fn(&AnyView, Box, &mut WindowContext), } impl AnyView { @@ -198,11 +197,11 @@ impl AnyView { cx: &mut WindowContext, ) { cx.with_absolute_element_offset(origin, |cx| { - let (layout_id, mut rendered_element) = (self.layout)(self, cx); + let (layout_id, rendered_element) = (self.layout)(self, cx); cx.window .layout_engine .compute_layout(layout_id, available_space); - (self.paint)(self, &mut rendered_element, cx); + (self.paint)(self, rendered_element, cx); }) } } @@ -224,7 +223,7 @@ impl From> for AnyView { } impl Element for AnyView { - type ElementState = Box; + type State = Option>; fn element_id(&self) -> Option { Some(self.model.entity_id.into()) @@ -233,27 +232,28 @@ impl Element for AnyView { fn layout( &mut self, _view_state: &mut ParentViewState, - _element_state: Option, + _element_state: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { - (self.layout)(self, cx) + ) -> (LayoutId, Self::State) { + let (layout_id, rendered_element) = (self.layout)(self, cx); + (layout_id, Some(rendered_element)) } fn paint( - &mut self, + mut self, _bounds: Bounds, _view_state: &mut ParentViewState, - rendered_element: &mut Self::ElementState, + rendered_element: &mut Self::State, cx: &mut ViewContext, ) { - (self.paint)(self, rendered_element, cx) + (self.paint)(&mut self, rendered_element.take().unwrap(), cx) } } pub struct AnyWeakView { model: AnyWeakModel, layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), - paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), + paint: fn(&AnyView, Box, &mut WindowContext), } impl AnyWeakView { @@ -311,7 +311,7 @@ where ParentViewState: 'static, ViewState: 'static, { - type ElementState = AnyElement; + type State = Option>; fn element_id(&self) -> Option { Some(self.view.entity_id().into()) @@ -320,24 +320,25 @@ where fn layout( &mut self, _: &mut ParentViewState, - _: Option, + _: Option, cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + ) -> (LayoutId, Self::State) { self.view.update(cx, |view, cx| { let mut element = self.component.take().unwrap().render(); let layout_id = element.layout(view, cx); - (layout_id, element) + (layout_id, Some(element)) }) } fn paint( - &mut self, + self, _: Bounds, _: &mut ParentViewState, - element: &mut Self::ElementState, + element: &mut Self::State, cx: &mut ViewContext, ) { - self.view.update(cx, |view, cx| element.paint(view, cx)) + self.view + .update(cx, |view, cx| element.take().unwrap().paint(view, cx)) } } @@ -359,14 +360,10 @@ mod any_view { }) } - pub(crate) fn paint( - view: &AnyView, - element: &mut Box, - cx: &mut WindowContext, - ) { + pub(crate) fn paint(view: &AnyView, element: Box, cx: &mut WindowContext) { cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); - let element = element.downcast_mut::>().unwrap(); + let element = element.downcast::>().unwrap(); view.update(cx, |view, cx| element.paint(view, cx)) }) } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 6d07f06d94..a9a2c60acb 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,5 +1,5 @@ use crate::{ - key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, + key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, @@ -237,7 +237,7 @@ pub struct Window { // #[derive(Default)] pub(crate) struct Frame { - pub(crate) element_states: HashMap, + pub(crate) element_states: HashMap>, mouse_listeners: HashMap>, pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index 3ce8373689..02c9629d01 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -1,5 +1,6 @@ mod action; mod derive_component; +mod derive_element; mod register_action; mod style_helpers; mod test; @@ -26,6 +27,11 @@ pub fn derive_component(input: TokenStream) -> TokenStream { derive_component::derive_component(input) } +// #[proc_macro_derive(Element)] +// pub fn derive_element(input: TokenStream) -> TokenStream { +// derive_element::derive_element(input) +// } + #[proc_macro_attribute] pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { test::test(args, function) diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index d3214cbff1..c6d2382f56 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -133,7 +133,7 @@ pub struct MenuHandleState { menu_element: Option>, } impl Element for MenuHandle { - type ElementState = MenuHandleState; + type State = MenuHandleState; fn element_id(&self) -> Option { Some(self.id.clone().expect("menu_handle must have an id()")) @@ -142,9 +142,9 @@ impl Element for MenuHandle { fn layout( &mut self, view_state: &mut V, - element_state: Option, + element_state: Option, cx: &mut crate::ViewContext, - ) -> (gpui::LayoutId, Self::ElementState) { + ) -> (gpui::LayoutId, Self::State) { let (menu, position) = if let Some(element_state) = element_state { (element_state.menu, element_state.position) } else { @@ -192,22 +192,22 @@ impl Element for MenuHandle { } fn paint( - &mut self, + self, bounds: Bounds, view_state: &mut V, - element_state: &mut Self::ElementState, + element_state: &mut Self::State, cx: &mut crate::ViewContext, ) { - if let Some(child) = element_state.child_element.as_mut() { + if let Some(child) = element_state.child_element.take() { child.paint(view_state, cx); } - if let Some(menu) = element_state.menu_element.as_mut() { + if let Some(menu) = element_state.menu_element.take() { menu.paint(view_state, cx); return; } - let Some(builder) = self.menu_builder.clone() else { + let Some(builder) = self.menu_builder else { return; }; let menu = element_state.menu.clone(); From 23ffce9fbe382931b13f740468541baa9eb88b84 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 18 Nov 2023 00:03:23 -0700 Subject: [PATCH 020/205] WIP: Work toward eliminating Component trait This refactor enhances the overall design by promoting reusable and composable UI component structures within the Zed project codebase. --- crates/gpui2/src/element.rs | 83 ++++++++++++++++++- crates/gpui2/src/interactive.rs | 7 +- crates/storybook2/src/stories/z_index.rs | 2 +- crates/theme2/src/story.rs | 17 ++-- crates/ui2/docs/hello-world.md | 10 +-- crates/ui2/src/components/avatar.rs | 2 +- crates/ui2/src/components/button.rs | 4 +- crates/ui2/src/components/checkbox.rs | 4 +- crates/ui2/src/components/details.rs | 2 +- crates/ui2/src/components/divider.rs | 2 +- crates/ui2/src/components/facepile.rs | 2 +- crates/ui2/src/components/icon.rs | 2 +- crates/ui2/src/components/icon_button.rs | 2 +- crates/ui2/src/components/indicator.rs | 2 +- crates/ui2/src/components/input.rs | 2 +- crates/ui2/src/components/keybinding.rs | 4 +- crates/ui2/src/components/label.rs | 4 +- crates/ui2/src/components/list.rs | 12 +-- crates/ui2/src/components/modal.rs | 2 +- .../ui2/src/components/notification_toast.rs | 2 +- crates/ui2/src/components/palette.rs | 4 +- crates/ui2/src/components/panel.rs | 2 +- crates/ui2/src/components/player_stack.rs | 2 +- crates/ui2/src/components/tab.rs | 2 +- crates/ui2/src/components/toast.rs | 33 +++++++- crates/ui2/src/components/toggle.rs | 4 +- crates/ui2/src/components/tool_divider.rs | 2 +- crates/ui2/src/story.rs | 6 +- crates/ui2/src/to_extract/assistant_panel.rs | 2 +- crates/ui2/src/to_extract/breadcrumb.rs | 2 +- crates/ui2/src/to_extract/buffer.rs | 6 +- crates/ui2/src/to_extract/chat_panel.rs | 4 +- crates/ui2/src/to_extract/collab_panel.rs | 2 +- crates/ui2/src/to_extract/command_palette.rs | 2 +- crates/ui2/src/to_extract/copilot.rs | 2 +- .../ui2/src/to_extract/language_selector.rs | 2 +- crates/ui2/src/to_extract/multi_buffer.rs | 2 +- .../ui2/src/to_extract/notifications_panel.rs | 8 +- crates/ui2/src/to_extract/panes.rs | 4 +- crates/ui2/src/to_extract/project_panel.rs | 2 +- crates/ui2/src/to_extract/recent_projects.rs | 2 +- crates/ui2/src/to_extract/tab_bar.rs | 2 +- crates/ui2/src/to_extract/terminal.rs | 2 +- crates/ui2/src/to_extract/theme_selector.rs | 2 +- crates/ui2/src/to_extract/toolbar.rs | 10 +-- crates/ui2/src/to_extract/traffic_lights.rs | 4 +- 46 files changed, 192 insertions(+), 90 deletions(-) diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index b9d312755a..5220892885 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -65,25 +65,26 @@ pub struct GlobalElementId(SmallVec<[ElementId; 32]>); pub trait ParentComponent { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; - fn child(mut self, child: impl Component) -> Self + fn child(mut self, child: impl Element) -> Self where Self: Sized, { - self.children_mut().push(child.render()); + self.children_mut().push(child.into_any()); self } - fn children(mut self, iter: impl IntoIterator>) -> Self + fn children(mut self, children: impl IntoIterator>) -> Self where Self: Sized, { self.children_mut() - .extend(iter.into_iter().map(|item| item.render())); + .extend(children.into_iter().map(Element::into_any)); self } } trait ElementObject { + fn element_id(&self) -> Option; fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); fn measure( @@ -132,6 +133,10 @@ impl> DrawableElement { } } + fn element_id(&self) -> Option { + self.element.as_ref()?.element_id() + } + fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id() { @@ -256,6 +261,10 @@ where E: Element, E::State: 'static, { + fn element_id(&self) -> Option { + self.as_ref().unwrap().element_id() + } + fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { DrawableElement::layout(self.as_mut().unwrap(), view_state, cx) } @@ -302,6 +311,10 @@ impl AnyElement { AnyElement(Box::new(Some(DrawableElement::new(element))) as Box>) } + pub fn element_id(&self) -> Option { + self.0.element_id() + } + pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { self.0.layout(view_state, cx) } @@ -332,6 +345,34 @@ impl AnyElement { } } +impl Element for AnyElement { + type State = (); + + fn element_id(&self) -> Option { + AnyElement::element_id(self) + } + + fn layout( + &mut self, + view_state: &mut V, + _: Option, + cx: &mut ViewContext, + ) -> (LayoutId, Self::State) { + let layout_id = self.layout(view_state, cx); + (layout_id, ()) + } + + fn paint( + self, + bounds: Bounds, + view_state: &mut V, + _: &mut Self::State, + cx: &mut ViewContext, + ) { + self.paint(view_state, cx); + } +} + pub trait Component { fn render(self) -> AnyElement; @@ -426,3 +467,37 @@ where AnyElement::new(Some(self)) } } + +// impl Element for F +// where +// V: 'static, +// E: 'static + Component, +// F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, +// { +// type State = Option>; + +// fn element_id(&self) -> Option { +// None +// } + +// fn layout( +// &mut self, +// view_state: &mut V, +// element_state: Option, +// cx: &mut ViewContext, +// ) -> (LayoutId, Self::State) { + +// self(view_state) + +// } + +// fn paint( +// self, +// bounds: Bounds, +// view_state: &mut V, +// element_state: &mut Self::State, +// cx: &mut ViewContext, +// ) { +// todo!() +// } +// } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 80a89ef625..83bcc5c8c5 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -307,12 +307,7 @@ mod test { .key_context("parent") .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true) - .child(|this: &mut Self, _cx: &mut ViewContext| { - div() - .key_context("nested") - .track_focus(&this.focus_handle) - .render() - }), + .child(div().key_context("nested").track_focus(&self.focus_handle)), ) } } diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 46ec0f4a35..02165db0cd 100644 --- a/crates/storybook2/src/stories/z_index.rs +++ b/crates/storybook2/src/stories/z_index.rs @@ -89,7 +89,7 @@ impl ZIndexExample { Self { z_index } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .relative() .size_full() diff --git a/crates/theme2/src/story.rs b/crates/theme2/src/story.rs index 4296d4f99c..9cf78cf943 100644 --- a/crates/theme2/src/story.rs +++ b/crates/theme2/src/story.rs @@ -1,4 +1,4 @@ -use gpui::{div, Component, Div, ParentComponent, Styled, ViewContext}; +use gpui::{div, Component, Div, Element, ParentComponent, SharedString, Styled, ViewContext}; use crate::ActiveTheme; @@ -16,23 +16,26 @@ impl Story { .bg(cx.theme().colors().background) } - pub fn title(cx: &mut ViewContext, title: &str) -> impl Component { + pub fn title(cx: &mut ViewContext, title: SharedString) -> impl Element { div() .text_xl() .text_color(cx.theme().colors().text) - .child(title.to_owned()) + .child(title) } - pub fn title_for(cx: &mut ViewContext) -> impl Component { - Self::title(cx, std::any::type_name::()) + pub fn title_for(cx: &mut ViewContext) -> impl Element { + Self::title(cx, std::any::type_name::().into()) } - pub fn label(cx: &mut ViewContext, label: &str) -> impl Component { + pub fn label( + cx: &mut ViewContext, + label: impl Into, + ) -> impl Element { div() .mt_4() .mb_2() .text_xs() .text_color(cx.theme().colors().text) - .child(label.to_owned()) + .child(label.into()) } } diff --git a/crates/ui2/docs/hello-world.md b/crates/ui2/docs/hello-world.md index e8ed3bb944..f48dd460b8 100644 --- a/crates/ui2/docs/hello-world.md +++ b/crates/ui2/docs/hello-world.md @@ -49,13 +49,13 @@ use gpui::hsla impl TodoList { // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0)) } } ~~~ -Every component needs a render method, and it should return `impl Component`. This basic component will render a 16x16px yellow square on the screen. +Every component needs a render method, and it should return `impl Element`. This basic component will render a 16x16px yellow square on the screen. A couple of questions might come to mind: @@ -87,7 +87,7 @@ We can access the current theme's colors like this: ~~~rust impl TodoList { // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let color = cx.theme().colors() div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0) @@ -102,7 +102,7 @@ use gpui::hsla impl TodoList { // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let color = cx.theme().colors() div().size_4().bg(color.surface) @@ -117,7 +117,7 @@ use gpui::hsla impl TodoList { // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let color = cx.theme().colors() div() diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index d083d8fd46..5ab00b33ed 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -21,7 +21,7 @@ impl Avatar { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let mut img = img(); if self.shape == Shape::Circle { diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index de055bcd5c..b2c6893793 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -164,7 +164,7 @@ impl Button { self.icon.map(|i| IconElement::new(i).color(icon_color)) } - pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let (icon_color, label_color) = match (self.disabled, self.color) { (true, _) => (TextColor::Disabled, TextColor::Disabled), (_, None) => (TextColor::Default, TextColor::Default), @@ -222,7 +222,7 @@ impl ButtonGroup { Self { buttons } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let mut el = h_stack().text_ui(); for button in self.buttons { diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index 5b9db17785..0b3141a9f3 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -1,4 +1,4 @@ -use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext}; +use gpui::{div, prelude::*, Component, Element, ElementId, Styled, ViewContext}; use std::sync::Arc; use theme2::ActiveTheme; @@ -42,7 +42,7 @@ impl Checkbox { self } - pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let group_id = format!("checkbox_group_{:?}", self.id); let icon = match self.checked { diff --git a/crates/ui2/src/components/details.rs b/crates/ui2/src/components/details.rs index f138290f17..41b3e45a95 100644 --- a/crates/ui2/src/components/details.rs +++ b/crates/ui2/src/components/details.rs @@ -27,7 +27,7 @@ impl Details { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { v_stack() .p_1() .gap_0p5() diff --git a/crates/ui2/src/components/divider.rs b/crates/ui2/src/components/divider.rs index 5ebfc7a4ff..7025a99a9f 100644 --- a/crates/ui2/src/components/divider.rs +++ b/crates/ui2/src/components/divider.rs @@ -31,7 +31,7 @@ impl Divider { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .map(|this| match self.direction { DividerDirection::Horizontal => { diff --git a/crates/ui2/src/components/facepile.rs b/crates/ui2/src/components/facepile.rs index efac4925f8..a3ec7db7b1 100644 --- a/crates/ui2/src/components/facepile.rs +++ b/crates/ui2/src/components/facepile.rs @@ -13,7 +13,7 @@ impl Facepile { } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let player_count = self.players.len(); let player_list = self.players.iter().enumerate().map(|(ix, player)| { let isnt_last = ix < player_count - 1; diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 61aa234978..7dc9385a4f 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -163,7 +163,7 @@ impl IconElement { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let svg_size = match self.size { IconSize::Small => rems(0.75), IconSize::Medium => rems(0.9375), diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 9b8548e3f9..5bb26cdb91 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -80,7 +80,7 @@ impl IconButton { self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) } - fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let icon_color = match (self.state, self.color) { (InteractionState::Disabled, _) => TextColor::Disabled, (InteractionState::Active, _) => TextColor::Selected, diff --git a/crates/ui2/src/components/indicator.rs b/crates/ui2/src/components/indicator.rs index 83030ebbee..02760852b1 100644 --- a/crates/ui2/src/components/indicator.rs +++ b/crates/ui2/src/components/indicator.rs @@ -10,7 +10,7 @@ impl UnreadIndicator { Self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .rounded_full() .border_2() diff --git a/crates/ui2/src/components/input.rs b/crates/ui2/src/components/input.rs index 42de03db12..1389d83ce2 100644 --- a/crates/ui2/src/components/input.rs +++ b/crates/ui2/src/components/input.rs @@ -55,7 +55,7 @@ impl Input { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let (input_bg, input_hover_bg, input_active_bg) = match self.variant { InputVariant::Ghost => ( cx.theme().colors().ghost_element_background, diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 8da5273bf5..608e3ab0b8 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -24,7 +24,7 @@ impl KeyBinding { Self { key_binding } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .flex() .gap_2() @@ -52,7 +52,7 @@ impl Key { Self { key: key.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .px_2() .py_0() diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 1beee5c8b7..c07c467000 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -100,7 +100,7 @@ impl Label { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .when(self.strikethrough, |this| { this.relative().child( @@ -161,7 +161,7 @@ impl HighlightedLabel { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let highlight_color = cx.theme().colors().text_accent; let mut text_style = cx.text_style().clone(); diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index b9508c5413..8756d2934c 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -57,7 +57,7 @@ impl ListHeader { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let disclosure_control = disclosure_control(self.toggle); let meta = match self.meta { @@ -138,7 +138,7 @@ impl ListSubHeader { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { h_stack().flex_1().w_full().relative().py_1().child( div() .h_6() @@ -198,7 +198,7 @@ impl From for ListItem { } impl ListItem { - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Element { match self { ListItem::Entry(entry) => div().child(entry.render(view, cx)), ListItem::Separator(separator) => div().child(separator.render(view, cx)), @@ -307,7 +307,7 @@ impl ListEntry { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let settings = user_settings(cx); let left_content = match self.left_slot.clone() { @@ -385,7 +385,7 @@ impl ListSeparator { Self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().h_px().w_full().bg(cx.theme().colors().border_variant) } } @@ -425,7 +425,7 @@ impl List { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let list_content = match (self.items.is_empty(), self.toggle) { (false, _) => div().children(self.items), (true, Toggle::Toggled(false)) => div(), diff --git a/crates/ui2/src/components/modal.rs b/crates/ui2/src/components/modal.rs index c3d71a78d8..ac4cb4017f 100644 --- a/crates/ui2/src/components/modal.rs +++ b/crates/ui2/src/components/modal.rs @@ -38,7 +38,7 @@ impl Modal { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { v_stack() .id(self.id.clone()) .w_96() diff --git a/crates/ui2/src/components/notification_toast.rs b/crates/ui2/src/components/notification_toast.rs index aeb2aa6ed9..ec0d2ac216 100644 --- a/crates/ui2/src/components/notification_toast.rs +++ b/crates/ui2/src/components/notification_toast.rs @@ -22,7 +22,7 @@ impl NotificationToast { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { h_stack() .z_index(5) .absolute() diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 5adf794a5e..4b6daea0f9 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -42,7 +42,7 @@ impl Palette { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { v_stack() .id(self.id.clone()) .w_96() @@ -135,7 +135,7 @@ impl PaletteItem { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .flex() .flex_row() diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index d9fc50dd92..88aca620da 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -92,7 +92,7 @@ impl Panel { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let current_size = self.width.unwrap_or(self.initial_width); v_stack() diff --git a/crates/ui2/src/components/player_stack.rs b/crates/ui2/src/components/player_stack.rs index 1a1231e6c4..b883f76d35 100644 --- a/crates/ui2/src/components/player_stack.rs +++ b/crates/ui2/src/components/player_stack.rs @@ -13,7 +13,7 @@ impl PlayerStack { } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let player = self.player_with_call_status.get_player(); let followers = self diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index 820fe5b361..7238dbc337 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -86,7 +86,7 @@ impl Tab { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict; let is_deleted = self.fs_status == FileSystemStatus::Deleted; diff --git a/crates/ui2/src/components/toast.rs b/crates/ui2/src/components/toast.rs index 0fcfe6038b..08e850d2a2 100644 --- a/crates/ui2/src/components/toast.rs +++ b/crates/ui2/src/components/toast.rs @@ -21,12 +21,41 @@ pub enum ToastOrigin { /// they are actively showing the a process in progress. /// /// Only one toast may be visible at a time. -#[derive(Component)] pub struct Toast { origin: ToastOrigin, children: SmallVec<[AnyElement; 2]>, } +impl Element for Toast { + type State = Option>; + + fn element_id(&self) -> Option { + None + } + + fn layout( + &mut self, + view_state: &mut V, + _element_state: Option, + cx: &mut ViewContext, + ) -> (gpui::LayoutId, Self::State) { + let mut element = self.render(view_state, cx).into_any(); + let layout_id = element.layout(view_state, cx); + (layout_id, Some(element)) + } + + fn paint( + self, + bounds: gpui::Bounds, + view_state: &mut V, + element: &mut Self::State, + cx: &mut ViewContext, + ) { + let element = element.take().unwrap(); + element.paint(view_state, cx); + } +} + impl Toast { pub fn new(origin: ToastOrigin) -> Self { Self { @@ -35,7 +64,7 @@ impl Toast { } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let mut div = div(); if self.origin == ToastOrigin::Bottom { diff --git a/crates/ui2/src/components/toggle.rs b/crates/ui2/src/components/toggle.rs index 8388e27531..5919a19fc6 100644 --- a/crates/ui2/src/components/toggle.rs +++ b/crates/ui2/src/components/toggle.rs @@ -1,4 +1,4 @@ -use gpui::{div, Component, ParentComponent}; +use gpui::{div, Component, Element, ParentComponent}; use crate::{Icon, IconElement, IconSize, TextColor}; @@ -44,7 +44,7 @@ impl From for Toggle { } } -pub fn disclosure_control(toggle: Toggle) -> impl Component { +pub fn disclosure_control(toggle: Toggle) -> impl Element { match (toggle.is_toggleable(), toggle.is_toggled()) { (false, _) => div(), (_, true) => div().child( diff --git a/crates/ui2/src/components/tool_divider.rs b/crates/ui2/src/components/tool_divider.rs index 8a9bbad97f..ca443ccb7f 100644 --- a/crates/ui2/src/components/tool_divider.rs +++ b/crates/ui2/src/components/tool_divider.rs @@ -8,7 +8,7 @@ impl ToolDivider { Self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().w_px().h_3().bg(cx.theme().colors().border) } } diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index c98cfa012f..b5fef606b7 100644 --- a/crates/ui2/src/story.rs +++ b/crates/ui2/src/story.rs @@ -15,18 +15,18 @@ impl Story { .bg(cx.theme().colors().background) } - pub fn title(cx: &mut ViewContext, title: &str) -> impl Component { + pub fn title(cx: &mut ViewContext, title: &str) -> impl Element { div() .text_xl() .text_color(cx.theme().colors().text) .child(title.to_owned()) } - pub fn title_for(cx: &mut ViewContext) -> impl Component { + pub fn title_for(cx: &mut ViewContext) -> impl Element { Self::title(cx, std::any::type_name::()) } - pub fn label(cx: &mut ViewContext, label: &str) -> impl Component { + pub fn label(cx: &mut ViewContext, label: &str) -> impl Element { div() .mt_4() .mb_2() diff --git a/crates/ui2/src/to_extract/assistant_panel.rs b/crates/ui2/src/to_extract/assistant_panel.rs index f111dad830..708d271b5b 100644 --- a/crates/ui2/src/to_extract/assistant_panel.rs +++ b/crates/ui2/src/to_extract/assistant_panel.rs @@ -21,7 +21,7 @@ impl AssistantPanel { self } - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Element { Panel::new(self.id.clone(), cx) .children(vec![div() .flex() diff --git a/crates/ui2/src/to_extract/breadcrumb.rs b/crates/ui2/src/to_extract/breadcrumb.rs index fd43a5b3bf..6990890054 100644 --- a/crates/ui2/src/to_extract/breadcrumb.rs +++ b/crates/ui2/src/to_extract/breadcrumb.rs @@ -22,7 +22,7 @@ impl Breadcrumb { .text_color(cx.theme().colors().text_muted) } - fn render(self, view_state: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, view_state: &mut V, cx: &mut ViewContext) -> impl Element { let symbols_len = self.symbols.len(); h_stack() diff --git a/crates/ui2/src/to_extract/buffer.rs b/crates/ui2/src/to_extract/buffer.rs index aa4bebc9d5..c8223daf2f 100644 --- a/crates/ui2/src/to_extract/buffer.rs +++ b/crates/ui2/src/to_extract/buffer.rs @@ -154,7 +154,7 @@ impl Buffer { self } - fn render_row(row: BufferRow, cx: &WindowContext) -> impl Component { + fn render_row(row: BufferRow, cx: &WindowContext) -> impl Element { let line_background = if row.current { cx.theme().colors().editor_active_line_background } else { @@ -202,7 +202,7 @@ impl Buffer { })) } - fn render_rows(&self, cx: &WindowContext) -> Vec> { + fn render_rows(&self, cx: &WindowContext) -> Vec> { match &self.rows { Some(rows) => rows .rows @@ -213,7 +213,7 @@ impl Buffer { } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let rows = self.render_rows(cx); v_stack() diff --git a/crates/ui2/src/to_extract/chat_panel.rs b/crates/ui2/src/to_extract/chat_panel.rs index 7e2846a3f6..528e40f903 100644 --- a/crates/ui2/src/to_extract/chat_panel.rs +++ b/crates/ui2/src/to_extract/chat_panel.rs @@ -21,7 +21,7 @@ impl ChatPanel { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .id(self.element_id.clone()) .flex() @@ -83,7 +83,7 @@ impl ChatMessage { } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .flex() .flex_col() diff --git a/crates/ui2/src/to_extract/collab_panel.rs b/crates/ui2/src/to_extract/collab_panel.rs index 256a648c0d..55efbb5ec5 100644 --- a/crates/ui2/src/to_extract/collab_panel.rs +++ b/crates/ui2/src/to_extract/collab_panel.rs @@ -14,7 +14,7 @@ impl CollabPanel { Self { id: id.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { v_stack() .id(self.id.clone()) .h_full() diff --git a/crates/ui2/src/to_extract/command_palette.rs b/crates/ui2/src/to_extract/command_palette.rs index 8a9461c796..353e1daac8 100644 --- a/crates/ui2/src/to_extract/command_palette.rs +++ b/crates/ui2/src/to_extract/command_palette.rs @@ -11,7 +11,7 @@ impl CommandPalette { Self { id: id.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().id(self.id.clone()).child( Palette::new("palette") .items(example_editor_actions()) diff --git a/crates/ui2/src/to_extract/copilot.rs b/crates/ui2/src/to_extract/copilot.rs index c5622f5be6..b07c562c13 100644 --- a/crates/ui2/src/to_extract/copilot.rs +++ b/crates/ui2/src/to_extract/copilot.rs @@ -10,7 +10,7 @@ impl CopilotModal { Self { id: id.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().id(self.id.clone()).child( Modal::new("some-id") .title("Connect Copilot to Zed") diff --git a/crates/ui2/src/to_extract/language_selector.rs b/crates/ui2/src/to_extract/language_selector.rs index 694ca78e9c..e05145e8c8 100644 --- a/crates/ui2/src/to_extract/language_selector.rs +++ b/crates/ui2/src/to_extract/language_selector.rs @@ -11,7 +11,7 @@ impl LanguageSelector { Self { id: id.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().id(self.id.clone()).child( Palette::new("palette") .items(vec![ diff --git a/crates/ui2/src/to_extract/multi_buffer.rs b/crates/ui2/src/to_extract/multi_buffer.rs index 78a22d51d0..186303365b 100644 --- a/crates/ui2/src/to_extract/multi_buffer.rs +++ b/crates/ui2/src/to_extract/multi_buffer.rs @@ -11,7 +11,7 @@ impl MultiBuffer { Self { buffers } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { v_stack() .w_full() .h_full() diff --git a/crates/ui2/src/to_extract/notifications_panel.rs b/crates/ui2/src/to_extract/notifications_panel.rs index f56194fc47..fa7f8c2bd5 100644 --- a/crates/ui2/src/to_extract/notifications_panel.rs +++ b/crates/ui2/src/to_extract/notifications_panel.rs @@ -15,7 +15,7 @@ impl NotificationsPanel { Self { id: id.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .id(self.id.clone()) .flex() @@ -241,7 +241,7 @@ impl Notification { self } - fn render_meta_items(&self, cx: &mut ViewContext) -> impl Component { + fn render_meta_items(&self, cx: &mut ViewContext) -> impl Element { if let Some(meta) = &self.meta { h_stack().children( meta.items @@ -260,14 +260,14 @@ impl Notification { } } - fn render_slot(&self, cx: &mut ViewContext) -> impl Component { + fn render_slot(&self, cx: &mut ViewContext) -> impl Element { match &self.slot { ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(), ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(), } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .relative() .id(self.id.clone()) diff --git a/crates/ui2/src/to_extract/panes.rs b/crates/ui2/src/to_extract/panes.rs index 288419d8bf..3b75d55d77 100644 --- a/crates/ui2/src/to_extract/panes.rs +++ b/crates/ui2/src/to_extract/panes.rs @@ -35,7 +35,7 @@ impl Pane { self } - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Element { div() .id(self.id.clone()) .flex() @@ -89,7 +89,7 @@ impl PaneGroup { } } - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Element { if !self.panes.is_empty() { let el = div() .flex() diff --git a/crates/ui2/src/to_extract/project_panel.rs b/crates/ui2/src/to_extract/project_panel.rs index 018f9a4bf1..ccaaecf2ce 100644 --- a/crates/ui2/src/to_extract/project_panel.rs +++ b/crates/ui2/src/to_extract/project_panel.rs @@ -14,7 +14,7 @@ impl ProjectPanel { Self { id: id.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .id(self.id.clone()) .flex() diff --git a/crates/ui2/src/to_extract/recent_projects.rs b/crates/ui2/src/to_extract/recent_projects.rs index 3d4f551490..2db7d94eac 100644 --- a/crates/ui2/src/to_extract/recent_projects.rs +++ b/crates/ui2/src/to_extract/recent_projects.rs @@ -11,7 +11,7 @@ impl RecentProjects { Self { id: id.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().id(self.id.clone()).child( Palette::new("palette") .items(vec![ diff --git a/crates/ui2/src/to_extract/tab_bar.rs b/crates/ui2/src/to_extract/tab_bar.rs index 3b4b5cc220..8888620dae 100644 --- a/crates/ui2/src/to_extract/tab_bar.rs +++ b/crates/ui2/src/to_extract/tab_bar.rs @@ -23,7 +23,7 @@ impl TabBar { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let (can_navigate_back, can_navigate_forward) = self.can_navigate; div() diff --git a/crates/ui2/src/to_extract/terminal.rs b/crates/ui2/src/to_extract/terminal.rs index 6c36f35152..d639986082 100644 --- a/crates/ui2/src/to_extract/terminal.rs +++ b/crates/ui2/src/to_extract/terminal.rs @@ -11,7 +11,7 @@ impl Terminal { Self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let can_navigate_back = true; let can_navigate_forward = false; diff --git a/crates/ui2/src/to_extract/theme_selector.rs b/crates/ui2/src/to_extract/theme_selector.rs index 7f911b50bf..175d126133 100644 --- a/crates/ui2/src/to_extract/theme_selector.rs +++ b/crates/ui2/src/to_extract/theme_selector.rs @@ -11,7 +11,7 @@ impl ThemeSelector { Self { id: id.into() } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().child( Palette::new(self.id.clone()) .items(vec![ diff --git a/crates/ui2/src/to_extract/toolbar.rs b/crates/ui2/src/to_extract/toolbar.rs index 81918f34a7..b7738ffb1c 100644 --- a/crates/ui2/src/to_extract/toolbar.rs +++ b/crates/ui2/src/to_extract/toolbar.rs @@ -20,7 +20,7 @@ impl Toolbar { } } - pub fn left_item(mut self, child: impl Component) -> Self + pub fn left_item(mut self, child: impl Element) -> Self where Self: Sized, { @@ -28,7 +28,7 @@ impl Toolbar { self } - pub fn left_items(mut self, iter: impl IntoIterator>) -> Self + pub fn left_items(mut self, iter: impl IntoIterator>) -> Self where Self: Sized, { @@ -37,7 +37,7 @@ impl Toolbar { self } - pub fn right_item(mut self, child: impl Component) -> Self + pub fn right_item(mut self, child: impl Element) -> Self where Self: Sized, { @@ -45,7 +45,7 @@ impl Toolbar { self } - pub fn right_items(mut self, iter: impl IntoIterator>) -> Self + pub fn right_items(mut self, iter: impl IntoIterator>) -> Self where Self: Sized, { @@ -54,7 +54,7 @@ impl Toolbar { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .bg(cx.theme().colors().toolbar_background) .p_2() diff --git a/crates/ui2/src/to_extract/traffic_lights.rs b/crates/ui2/src/to_extract/traffic_lights.rs index 245ff377f2..8cab5594e6 100644 --- a/crates/ui2/src/to_extract/traffic_lights.rs +++ b/crates/ui2/src/to_extract/traffic_lights.rs @@ -21,7 +21,7 @@ impl TrafficLight { } } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let system_colors = &cx.theme().styles.system; let fill = match (self.window_has_focus, self.color) { @@ -52,7 +52,7 @@ impl TrafficLights { self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div() .flex() .items_center() From be33f000e2cfda239637db52d645bffa93aef804 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 18 Nov 2023 00:27:40 -0700 Subject: [PATCH 021/205] WIP: Lots of errors, starting on resurrecting derive Element --- crates/gpui2/src/element.rs | 2 +- crates/gpui2_macros/src/derive_element.rs | 75 +++++++++++++++++++++++ crates/gpui2_macros/src/gpui2_macros.rs | 18 +++--- crates/ui2/src/components/avatar.rs | 5 +- crates/ui2/src/components/toast.rs | 54 ++++++++-------- 5 files changed, 115 insertions(+), 39 deletions(-) create mode 100644 crates/gpui2_macros/src/derive_element.rs diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 5220892885..f296e9214c 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -364,7 +364,7 @@ impl Element for AnyElement { fn paint( self, - bounds: Bounds, + _bounds: Bounds, view_state: &mut V, _: &mut Self::State, cx: &mut ViewContext, diff --git a/crates/gpui2_macros/src/derive_element.rs b/crates/gpui2_macros/src/derive_element.rs new file mode 100644 index 0000000000..e8c698c1fe --- /dev/null +++ b/crates/gpui2_macros/src/derive_element.rs @@ -0,0 +1,75 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, GenericParam}; + +pub fn derive_element(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_name = ast.ident; + + let mut view_state_ty = quote! { V }; + + for param in &ast.generics.params { + if let GenericParam::Type(type_param) = param { + let type_ident = &type_param.ident; + view_state_ty = quote! {#type_ident}; + break; + } + } + + let attrs = &ast.attrs; + for attr in attrs { + if attr.path.is_ident("element") { + match attr.parse_meta() { + Ok(syn::Meta::List(i)) => { + for nested_meta in i.nested { + if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = nested_meta { + if nv.path.is_ident("view_state") { + if let syn::Lit::Str(lit_str) = nv.lit { + view_state_ty = lit_str.value().parse().unwrap(); + } + } + } + } + } + _ => (), + } + } + } + + let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); + + let gen = quote! { + impl #impl_generics gpui::Element<#view_state_ty> for #type_name #ty_generics + #where_clause + { + type State = Option>; + + fn element_id(&self) -> Option { + None + } + + fn layout( + &mut self, + view_state: &mut #view_state_ty, + _element_state: Option, + cx: &mut gpui::ViewContext<#view_state_ty>, + ) -> (gpui::LayoutId, Self::State) { + let mut element = self.render(view_state, cx).into_any(); + let layout_id = element.layout(view_state, cx); + (layout_id, Some(element)) + } + + fn paint( + self, + _bounds: gpui::Bounds, + view_state: &mut #view_state_ty, + rendered_element: &mut Self::State, + cx: &mut gpui::ViewContext<#view_state_ty>, + ) { + rendered_element.take().unwrap().paint(view_state, cx) + } + } + }; + + gen.into() +} diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index 02c9629d01..a611727066 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -7,11 +7,6 @@ mod test; use proc_macro::TokenStream; -#[proc_macro] -pub fn style_helpers(args: TokenStream) -> TokenStream { - style_helpers::style_helpers(args) -} - #[proc_macro_derive(Action)] pub fn action(input: TokenStream) -> TokenStream { action::action(input) @@ -27,10 +22,15 @@ pub fn derive_component(input: TokenStream) -> TokenStream { derive_component::derive_component(input) } -// #[proc_macro_derive(Element)] -// pub fn derive_element(input: TokenStream) -> TokenStream { -// derive_element::derive_element(input) -// } +#[proc_macro_derive(Element, attributes(element))] +pub fn derive_element(input: TokenStream) -> TokenStream { + derive_element::derive_element(input) +} + +#[proc_macro] +pub fn style_helpers(input: TokenStream) -> TokenStream { + style_helpers::style_helpers(input) +} #[proc_macro_attribute] pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 5ab00b33ed..95e0653d36 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,8 +1,7 @@ +use crate::prelude::*; use gpui::img; -use crate::prelude::*; - -#[derive(Component)] +#[derive(Element)] pub struct Avatar { src: SharedString, shape: Shape, diff --git a/crates/ui2/src/components/toast.rs b/crates/ui2/src/components/toast.rs index 08e850d2a2..a40f3cace4 100644 --- a/crates/ui2/src/components/toast.rs +++ b/crates/ui2/src/components/toast.rs @@ -1,4 +1,5 @@ use crate::prelude::*; +use gpui::Element; use gpui::{prelude::*, AnyElement}; use smallvec::SmallVec; @@ -21,40 +22,41 @@ pub enum ToastOrigin { /// they are actively showing the a process in progress. /// /// Only one toast may be visible at a time. +#[derive(Element)] pub struct Toast { origin: ToastOrigin, children: SmallVec<[AnyElement; 2]>, } -impl Element for Toast { - type State = Option>; +// impl Element for Toast { +// type State = Option>; - fn element_id(&self) -> Option { - None - } +// fn element_id(&self) -> Option { +// None +// } - fn layout( - &mut self, - view_state: &mut V, - _element_state: Option, - cx: &mut ViewContext, - ) -> (gpui::LayoutId, Self::State) { - let mut element = self.render(view_state, cx).into_any(); - let layout_id = element.layout(view_state, cx); - (layout_id, Some(element)) - } +// fn layout( +// &mut self, +// view_state: &mut V, +// _element_state: Option, +// cx: &mut ViewContext, +// ) -> (gpui::LayoutId, Self::State) { +// let mut element = self.render(view_state, cx).into_any(); +// let layout_id = element.layout(view_state, cx); +// (layout_id, Some(element)) +// } - fn paint( - self, - bounds: gpui::Bounds, - view_state: &mut V, - element: &mut Self::State, - cx: &mut ViewContext, - ) { - let element = element.take().unwrap(); - element.paint(view_state, cx); - } -} +// fn paint( +// self, +// bounds: gpui::Bounds, +// view_state: &mut V, +// element: &mut Self::State, +// cx: &mut ViewContext, +// ) { +// let element = element.take().unwrap(); +// element.paint(view_state, cx); +// } +// } impl Toast { pub fn new(origin: ToastOrigin) -> Self { From adc355a1e693fc63088118b542943fabc89a74f1 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 18 Nov 2023 20:05:47 -0700 Subject: [PATCH 022/205] Element refinement passing on ui2 --- crates/collab_ui2/src/collab_panel.rs | 2 +- crates/collab_ui2/src/collab_titlebar_item.rs | 6 +- .../command_palette2/src/command_palette.rs | 2 +- crates/editor2/src/editor.rs | 12 +- crates/editor2/src/editor_tests.rs | 2 +- crates/editor2/src/element.rs | 6 +- crates/editor2/src/items.rs | 2 +- crates/file_finder2/src/file_finder.rs | 4 +- crates/go_to_line2/src/go_to_line.rs | 2 +- crates/gpui2/src/app.rs | 2 +- crates/gpui2/src/app/async_context.rs | 6 +- crates/gpui2/src/app/test_context.rs | 12 +- crates/gpui2/src/element.rs | 271 +++++++++++------- crates/gpui2/src/elements/div.rs | 80 +++--- crates/gpui2/src/elements/img.rs | 21 +- crates/gpui2/src/elements/overlay.rs | 20 +- crates/gpui2/src/elements/svg.rs | 21 +- crates/gpui2/src/elements/text.rs | 88 +++--- crates/gpui2/src/elements/uniform_list.rs | 30 +- crates/gpui2/src/gpui2.rs | 4 +- crates/gpui2/src/interactive.rs | 14 +- crates/gpui2/src/prelude.rs | 5 +- crates/gpui2/src/view.rs | 156 ++++++---- crates/gpui2/src/window.rs | 18 +- crates/gpui2_macros/src/derive_element.rs | 75 ----- crates/gpui2_macros/src/derive_render_once.rs | 64 +++++ crates/gpui2_macros/src/gpui2_macros.rs | 8 +- crates/project_panel2/src/project_panel.rs | 6 +- crates/storybook2/src/stories/text.rs | 4 +- crates/storybook2/src/stories/z_index.rs | 2 +- crates/terminal_view2/src/terminal_panel.rs | 2 +- crates/terminal_view2/src/terminal_view.rs | 7 +- crates/theme2/src/story.rs | 2 +- crates/theme2/src/styles/players.rs | 4 +- crates/ui2/src/components/avatar.rs | 36 +-- crates/ui2/src/components/button.rs | 87 +++++- crates/ui2/src/components/checkbox.rs | 130 ++++++++- crates/ui2/src/components/context_menu.rs | 29 +- crates/ui2/src/components/details.rs | 33 ++- crates/ui2/src/components/divider.rs | 4 +- crates/ui2/src/components/facepile.rs | 23 +- crates/ui2/src/components/icon.rs | 23 +- crates/ui2/src/components/icon_button.rs | 110 +++---- crates/ui2/src/components/indicator.rs | 20 +- crates/ui2/src/components/input.rs | 104 +++---- crates/ui2/src/components/keybinding.rs | 54 ++-- crates/ui2/src/components/label.rs | 121 ++++---- crates/ui2/src/components/list.rs | 169 ++++++++--- crates/ui2/src/components/modal.rs | 63 ++-- .../ui2/src/components/notification_toast.rs | 2 +- crates/ui2/src/components/palette.rs | 165 ++++++----- crates/ui2/src/components/panel.rs | 54 ++-- crates/ui2/src/components/player_stack.rs | 22 +- crates/ui2/src/components/tab.rs | 128 +++++---- crates/ui2/src/components/toast.rs | 58 ++-- crates/ui2/src/components/toggle.rs | 2 +- crates/ui2/src/components/tool_divider.rs | 11 +- crates/ui2/src/components/tooltip.rs | 4 +- crates/ui2/src/prelude.rs | 4 +- crates/ui2/src/static_data.rs | 66 ++--- crates/ui2/src/story.rs | 14 +- crates/ui2/src/to_extract/assistant_panel.rs | 38 +-- crates/ui2/src/to_extract/breadcrumb.rs | 45 +-- crates/ui2/src/to_extract/buffer.rs | 25 +- crates/ui2/src/to_extract/buffer_search.rs | 5 +- crates/ui2/src/to_extract/chat_panel.rs | 60 ++-- crates/ui2/src/to_extract/collab_panel.rs | 20 +- crates/ui2/src/to_extract/command_palette.rs | 19 +- crates/ui2/src/to_extract/copilot.rs | 33 ++- crates/ui2/src/to_extract/editor_pane.rs | 4 +- .../ui2/src/to_extract/language_selector.rs | 30 +- crates/ui2/src/to_extract/multi_buffer.rs | 19 +- .../ui2/src/to_extract/notifications_panel.rs | 180 ++++++------ crates/ui2/src/to_extract/panes.rs | 87 +++--- crates/ui2/src/to_extract/project_panel.rs | 42 ++- crates/ui2/src/to_extract/recent_projects.rs | 19 +- crates/ui2/src/to_extract/status_bar.rs | 48 ++-- crates/ui2/src/to_extract/tab_bar.rs | 38 +-- crates/ui2/src/to_extract/terminal.rs | 75 ++++- crates/ui2/src/to_extract/theme_selector.rs | 20 +- crates/ui2/src/to_extract/title_bar.rs | 6 +- crates/ui2/src/to_extract/toolbar.rs | 54 ++-- crates/ui2/src/to_extract/traffic_lights.rs | 55 ++-- crates/ui2/src/to_extract/workspace.rs | 6 +- crates/workspace2/src/dock.rs | 2 +- crates/workspace2/src/status_bar.rs | 4 +- crates/workspace2/src/workspace2.rs | 8 +- 87 files changed, 2066 insertions(+), 1372 deletions(-) delete mode 100644 crates/gpui2_macros/src/derive_element.rs create mode 100644 crates/gpui2_macros/src/derive_render_once.rs diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 20e77d7023..22f6dc9d88 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -160,7 +160,7 @@ use std::sync::Arc; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, InteractiveComponent, ParentComponent, Render, View, ViewContext, + Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext, VisualContext, WeakView, }; use project::Fs; diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index ed010cc500..570ff9f45e 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,9 +31,9 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, - Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, - VisualContext, WeakView, WindowBounds, + div, px, rems, AppContext, Component, Div, InteractiveElement, Model, ParentElement, Render, + Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, + WeakView, WindowBounds, }; use project::Project; use theme::ActiveTheme; diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 9463cab68c..7e4994638e 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -2,7 +2,7 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke, - ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, + ManagedView, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::{ diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index b1dc76852d..214c46551f 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -42,7 +42,7 @@ use gpui::{ actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, - Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled, + Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -1595,7 +1595,7 @@ impl CodeActionsMenu { .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) .map(|(ix, _)| ix), ) - .render(); + .render_once(); if self.deployed_from_indicator { *cursor_position.column_mut() = 0; @@ -4365,7 +4365,7 @@ impl Editor { cx, ); }) - .render(), + .into_any(), ) } else { None @@ -4401,7 +4401,7 @@ impl Editor { editor.fold_at(&FoldAt { buffer_row }, cx); } }) - .render() + .into_any() }) }) .flatten() @@ -7792,7 +7792,7 @@ impl Editor { cx.editor_style.diagnostic_style.clone(), }, ))) - .render() + .render_once() } }), disposition: BlockDisposition::Below, @@ -9994,7 +9994,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend cx.write_to_clipboard(ClipboardItem::new(message.clone())); }) .tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx)) - .render() + .render_once() }) } diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index bd69e7acdf..f23618aa21 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { position: snapshot.anchor_after(Point::new(2, 0)), disposition: BlockDisposition::Below, height: 1, - render: Arc::new(|_| div().render()), + render: Arc::new(|_| div().render_once()), }], Some(Autoscroll::fit()), cx, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 2de9182901..0ee710da47 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -20,9 +20,9 @@ use collections::{BTreeMap, HashMap}; use gpui::{ div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, - ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, - ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled, + ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, LineLayout, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine, }; use itertools::Itertools; diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index cf2bf5b6dc..ef0f48193c 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -9,7 +9,7 @@ use collections::HashSet; use futures::future::try_join_all; use gpui::{ div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, - FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View, + FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::{ diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 0fee5102e6..3488b6916c 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,8 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent, - ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, + actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveElement, + ManagedView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 565afb5e93..26641bfa09 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,6 +1,6 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent, + actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 397b770c2b..0a1e571872 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -424,7 +424,7 @@ impl AppContext { /// Opens a new window with the given option and the root view returned by the given function. /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific /// functionality. - pub fn open_window( + pub fn open_window>( &mut self, options: crate::WindowOptions, build_root_view: impl FnOnce(&mut WindowContext) -> View, diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 83b3ccebe7..afb6050037 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -115,7 +115,7 @@ impl AsyncAppContext { build_root_view: impl FnOnce(&mut WindowContext) -> View, ) -> Result> where - V: Render, + V: 'static + Render, { let app = self .app @@ -286,7 +286,7 @@ impl VisualContext for AsyncWindowContext { build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { self.window .update(self, |_, cx| cx.build_view(build_view_state)) @@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: Render, + V: 'static + Render, { self.window .update(self, |_, cx| cx.replace_root_view(build_view)) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 940492573f..06893a1772 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -126,7 +126,7 @@ impl TestAppContext { pub fn add_window(&mut self, build_window: F) -> WindowHandle where F: FnOnce(&mut ViewContext) -> V, - V: Render, + V: 'static + Render, { let mut cx = self.app.borrow_mut(); cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) @@ -143,7 +143,7 @@ impl TestAppContext { pub fn add_window_view(&mut self, build_window: F) -> (View, &mut VisualTestContext) where F: FnOnce(&mut ViewContext) -> V, - V: Render, + V: 'static + Render, { let mut cx = self.app.borrow_mut(); let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)); @@ -543,7 +543,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { self.window .update(self.cx, |_, cx| cx.build_view(build_view)) @@ -565,7 +565,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: Render, + V: 'static + Render, { self.window .update(self.cx, |_, cx| cx.replace_root_view(build_view)) @@ -582,7 +582,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { } impl AnyWindowHandle { - pub fn build_view( + pub fn build_view + 'static>( &self, cx: &mut TestAppContext, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, @@ -593,7 +593,7 @@ impl AnyWindowHandle { pub struct EmptyView {} -impl Render for EmptyView { +impl Render for EmptyView { type Element = Div; fn render(&mut self, _cx: &mut crate::ViewContext) -> Self::Element { diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index f296e9214c..c60c902e25 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -3,9 +3,53 @@ use crate::{ }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; -use std::{any::Any, fmt::Debug}; +use std::{any::Any, fmt::Debug, marker::PhantomData}; -pub trait Element: 'static + Sized { +pub trait Render: 'static + Sized { + type Element: Element + 'static; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element; +} + +pub trait RenderOnce: Sized { + type Element: Element + 'static; + + fn render_once(self) -> Self::Element; + + fn render_into_any(self) -> AnyElement { + self.render_once().into_any() + } + + fn map(self, f: impl FnOnce(Self) -> U) -> U + where + Self: Sized, + U: RenderOnce, + { + f(self) + } + + fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| if condition { then(this) } else { this }) + } + + fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self + where + Self: Sized, + { + self.map(|this| { + if let Some(value) = option { + then(this, value) + } else { + this + } + }) + } +} + +pub trait Element: 'static + RenderOnce { type State: 'static; fn element_id(&self) -> Option; @@ -59,26 +103,105 @@ pub trait Element: 'static + Sized { } } +pub trait Component: 'static { + type Rendered: RenderOnce; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered; +} + +pub struct CompositeElement { + component: Option, + view_type: PhantomData, +} + +pub struct CompositeElementState> { + rendered_element: Option<>::Element>, + rendered_element_state: <>::Element as Element>::State, +} + +impl CompositeElement { + pub fn new(component: C) -> Self { + CompositeElement { + component: Some(component), + view_type: PhantomData, + } + } +} + +impl> Element for CompositeElement { + type State = CompositeElementState; + + fn element_id(&self) -> Option { + None + } + + fn layout( + &mut self, + view: &mut V, + state: Option, + cx: &mut ViewContext, + ) -> (LayoutId, Self::State) { + let mut element = self + .component + .take() + .unwrap() + .render(view, cx) + .render_once(); + let (layout_id, state) = element.layout(view, state.map(|s| s.rendered_element_state), cx); + let state = CompositeElementState { + rendered_element: Some(element), + rendered_element_state: state, + }; + (layout_id, state) + } + + fn paint( + self, + bounds: Bounds, + view: &mut V, + state: &mut Self::State, + cx: &mut ViewContext, + ) { + state.rendered_element.take().unwrap().paint( + bounds, + view, + &mut state.rendered_element_state, + cx, + ); + } +} + +impl> RenderOnce for CompositeElement { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] pub struct GlobalElementId(SmallVec<[ElementId; 32]>); -pub trait ParentComponent { +pub trait ParentElement { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; - fn child(mut self, child: impl Element) -> Self + fn child(mut self, child: impl RenderOnce) -> Self where Self: Sized, { - self.children_mut().push(child.into_any()); + self.children_mut().push(child.render_once().into_any()); self } - fn children(mut self, children: impl IntoIterator>) -> Self + fn children(mut self, children: impl IntoIterator>) -> Self where Self: Sized, { - self.children_mut() - .extend(children.into_iter().map(Element::into_any)); + self.children_mut().extend( + children + .into_iter() + .map(|child| child.render_once().into_any()), + ); self } } @@ -301,7 +424,7 @@ where pub struct AnyElement(Box>); -impl AnyElement { +impl AnyElement { pub fn new(element: E) -> Self where V: 'static, @@ -343,6 +466,11 @@ impl AnyElement { ) { self.0.draw(origin, available_space, view_state, cx) } + + /// Converts this `AnyElement` into a trait object that can be stored and manipulated. + pub fn into_any(self) -> AnyElement { + AnyElement::new(self) + } } impl Element for AnyElement { @@ -373,105 +501,18 @@ impl Element for AnyElement { } } -pub trait Component { - fn render(self) -> AnyElement; +impl RenderOnce for AnyElement { + type Element = Self; - fn map(self, f: impl FnOnce(Self) -> U) -> U - where - Self: Sized, - U: Component, - { - f(self) - } - - fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { this }) - } - - fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| { - if let Some(value) = option { - then(this, value) - } else { - this - } - }) - } -} - -impl Component for AnyElement { - fn render(self) -> AnyElement { + fn render_once(self) -> Self::Element { self } } -impl Element for Option -where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, -{ - type State = Option>; - - fn element_id(&self) -> Option { - None - } - - fn layout( - &mut self, - view_state: &mut V, - _: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::State) { - let render = self.take().unwrap(); - let mut rendered_element = (render)(view_state, cx).render(); - let layout_id = rendered_element.layout(view_state, cx); - (layout_id, Some(rendered_element)) - } - - fn paint( - self, - _bounds: Bounds, - view_state: &mut V, - rendered_element: &mut Self::State, - cx: &mut ViewContext, - ) { - rendered_element.take().unwrap().paint(view_state, cx); - } -} - -impl Component for Option -where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, -{ - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl Component for F -where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, -{ - fn render(self) -> AnyElement { - AnyElement::new(Some(self)) - } -} - -// impl Element for F +// impl Element for Option // where // V: 'static, -// E: 'static + Component, +// E: Element, // F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, // { // type State = Option>; @@ -483,21 +524,35 @@ where // fn layout( // &mut self, // view_state: &mut V, -// element_state: Option, +// _: Option, // cx: &mut ViewContext, // ) -> (LayoutId, Self::State) { - -// self(view_state) - +// let render = self.take().unwrap(); +// let mut element = (render)(view_state, cx).into_any(); +// let layout_id = element.layout(view_state, cx); +// (layout_id, Some(element)) // } // fn paint( // self, -// bounds: Bounds, +// _bounds: Bounds, // view_state: &mut V, -// element_state: &mut Self::State, +// rendered_element: &mut Self::State, // cx: &mut ViewContext, // ) { -// todo!() +// rendered_element.take().unwrap().paint(view_state, cx); +// } +// } + +// impl RenderOnce for Option +// where +// V: 'static, +// E: Element, +// F: FnOnce(&mut V, &mut ViewContext) -> E + 'static, +// { +// type Element = Self; + +// fn render(self) -> Self::Element { +// self // } // } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 1a70a9bab2..26de4ea25f 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,9 +1,9 @@ use crate::{ point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, - BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent, - FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent, - SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, + BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, + KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, ParentElement, Pixels, Point, Render, RenderOnce, ScrollWheelEvent, SharedString, + Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, }; use collections::HashMap; use refineable::Refineable; @@ -28,7 +28,7 @@ pub struct GroupStyle { pub style: StyleRefinement, } -pub trait InteractiveComponent: Sized + Element { +pub trait InteractiveElement: Sized + Element { fn interactivity(&mut self) -> &mut Interactivity; fn group(mut self, group: impl Into) -> Self { @@ -314,7 +314,7 @@ pub trait InteractiveComponent: Sized + Element { } } -pub trait StatefulInteractiveComponent>: InteractiveComponent { +pub trait StatefulInteractiveElement>: InteractiveElement { fn focusable(mut self) -> Focusable { self.interactivity().focusable = true; Focusable { @@ -381,7 +381,7 @@ pub trait StatefulInteractiveComponent>: InteractiveCo ) -> Self where Self: Sized, - W: 'static + Render, + W: 'static + Render, { debug_assert!( self.interactivity().drag_listener.is_none(), @@ -425,7 +425,7 @@ pub trait StatefulInteractiveComponent>: InteractiveCo } } -pub trait FocusableComponent: InteractiveComponent { +pub trait FocusableElement: InteractiveElement { fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -587,13 +587,13 @@ impl Styled for Div { } } -impl InteractiveComponent for Div { +impl InteractiveElement for Div { fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } -impl ParentComponent for Div { +impl ParentElement for Div { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } @@ -691,9 +691,11 @@ impl Element for Div { } } -impl Component for Div { - fn render(self) -> AnyElement { - AnyElement::new(self) +impl RenderOnce for Div { + type Element = Self; + + fn render_once(self) -> Self::Element { + self } } @@ -1257,19 +1259,19 @@ pub struct Focusable { view_type: PhantomData, } -impl> FocusableComponent for Focusable {} +impl, E: InteractiveElement> FocusableElement for Focusable {} -impl InteractiveComponent for Focusable +impl InteractiveElement for Focusable where - V: 'static, - E: InteractiveComponent, + V: 'static + Render, + E: InteractiveElement, { fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl> StatefulInteractiveComponent +impl, E: StatefulInteractiveElement> StatefulInteractiveElement for Focusable { } @@ -1286,7 +1288,7 @@ where impl Element for Focusable where - V: 'static, + V: 'static + Render, E: Element, { type State = E::State; @@ -1315,20 +1317,22 @@ where } } -impl Component for Focusable +impl RenderOnce for Focusable where - V: 'static, - E: 'static + Element, + V: 'static + Render, + E: Element, { - fn render(self) -> AnyElement { - AnyElement::new(self) + type Element = Self; + + fn render_once(self) -> Self::Element { + self } } -impl ParentComponent for Focusable +impl ParentElement for Focusable where V: 'static, - E: ParentComponent, + E: ParentElement, { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { self.element.children_mut() @@ -1350,25 +1354,25 @@ where } } -impl StatefulInteractiveComponent for Stateful +impl StatefulInteractiveElement for Stateful where V: 'static, E: Element, - Self: InteractiveComponent, + Self: InteractiveElement, { } -impl InteractiveComponent for Stateful +impl InteractiveElement for Stateful where V: 'static, - E: InteractiveComponent, + E: InteractiveElement, { fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl> FocusableComponent for Stateful {} +impl> FocusableElement for Stateful {} impl Element for Stateful where @@ -1401,20 +1405,22 @@ where } } -impl Component for Stateful +impl RenderOnce for Stateful where V: 'static, - E: 'static + Element, + E: Element, { - fn render(self) -> AnyElement { - AnyElement::new(self) + type Element = Self; + + fn render_once(self) -> Self::Element { + self } } -impl ParentComponent for Stateful +impl ParentElement for Stateful where V: 'static, - E: ParentComponent, + E: ParentElement, { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { self.element.children_mut() diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index db55bd0dc6..16f20869ab 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,7 +1,6 @@ use crate::{ - AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent, - InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, - Styled, ViewContext, + BorrowWindow, Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, + LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, ViewContext, }; use futures::FutureExt; use util::ResultExt; @@ -35,12 +34,6 @@ where } } -impl Component for Img { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - impl Element for Img { type State = InteractiveElementState; @@ -102,13 +95,21 @@ impl Element for Img { } } +impl RenderOnce for Img { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + impl Styled for Img { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveComponent for Img { +impl InteractiveElement for Img { fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 394030f3d7..4e01b8aad1 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -2,8 +2,8 @@ use smallvec::SmallVec; use taffy::style::{Display, Position}; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, - Point, Size, Style, + point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point, + RenderOnce, Size, Style, }; pub struct OverlayState { @@ -51,18 +51,12 @@ impl Overlay { } } -impl ParentComponent for Overlay { +impl ParentElement for Overlay { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } } -impl Component for Overlay { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - impl Element for Overlay { type State = OverlayState; @@ -163,6 +157,14 @@ impl Element for Overlay { } } +impl RenderOnce for Overlay { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + enum Axis { Horizontal, Vertical, diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 7d9c04b729..f6823c50d5 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,7 +1,6 @@ use crate::{ - AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent, - InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, - Styled, ViewContext, + Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity, + LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, ViewContext, }; use util::ResultExt; @@ -24,12 +23,6 @@ impl Svg { } } -impl Component for Svg { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - impl Element for Svg { type State = InteractiveElementState; @@ -66,13 +59,21 @@ impl Element for Svg { } } +impl RenderOnce for Svg { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + impl Styled for Svg { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveComponent for Svg { +impl InteractiveElement for Svg { fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index c6090fdfb4..bfd63d3c3d 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,6 +1,6 @@ use crate::{ - AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels, - SharedString, Size, TextRun, ViewContext, WindowContext, WrappedLine, + BorrowWindow, Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, + TextRun, ViewContext, WindowContext, WrappedLine, }; use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; @@ -37,6 +37,14 @@ impl Element for &'static str { } } +impl RenderOnce for &'static str { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + impl Element for SharedString { type State = TextState; @@ -67,32 +75,34 @@ impl Element for SharedString { } } -pub struct Text { +impl RenderOnce for SharedString { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + +pub struct StyledText { text: SharedString, runs: Option>, } -impl Text { +impl StyledText { /// Renders text with runs of different styles. /// /// Callers are responsible for setting the correct style for each run. /// For text with a uniform style, you can usually avoid calling this constructor /// and just pass text directly. - pub fn styled(text: SharedString, runs: Vec) -> Self { - Text { + pub fn new(text: SharedString, runs: Vec) -> Self { + StyledText { text, runs: Some(runs), } } } -impl Component for Text { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl Element for Text { +impl Element for StyledText { type State = TextState; fn element_id(&self) -> Option { @@ -181,9 +191,22 @@ impl Element for Text { } } +impl RenderOnce for StyledText { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + #[derive(Default, Clone)] pub struct TextState(Arc>>); +struct TextStateInner { + lines: SmallVec<[WrappedLine; 1]>, + line_height: Pixels, +} + impl TextState { fn lock(&self) -> MutexGuard> { self.0.lock() @@ -264,14 +287,9 @@ impl TextState { } } -struct TextStateInner { - lines: SmallVec<[WrappedLine; 1]>, - line_height: Pixels, -} - struct InteractiveText { id: ElementId, - text: Text, + text: StyledText, } struct InteractiveTextState { @@ -325,34 +343,10 @@ impl Element for InteractiveText { } } -impl Component for SharedString { - fn render(self) -> AnyElement { - Text { - text: self, - runs: None, - } - .render() - } -} +impl RenderOnce for InteractiveText { + type Element = Self; -impl Component for &'static str { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - } - .render() - } -} - -// TODO: Figure out how to pass `String` to `child` without this. -// This impl doesn't exist in the `gpui2` crate. -impl Component for String { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - } - .render() + fn render_once(self) -> Self::Element { + self } } diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index d7339d00ab..997904d913 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,7 +1,7 @@ use crate::{ - point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, - ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, - Point, Size, StyleRefinement, Styled, ViewContext, + point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Element, ElementId, + InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels, Point, + RenderOnce, Size, StyleRefinement, Styled, ViewContext, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -10,15 +10,15 @@ use taffy::style::Overflow; /// uniform_list provides lazy rendering for a set of items that are of uniform height. /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, /// uniform_list will only render the visibile subset of items. -pub fn uniform_list( +pub fn uniform_list( id: I, item_count: usize, - f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, + f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, ) -> UniformList where I: Into, V: 'static, - C: Component, + E: Element, { let id = id.into(); let mut style = StyleRefinement::default(); @@ -32,7 +32,7 @@ where render_items: Box::new(move |view, visible_range, cx| { f(view, visible_range, cx) .into_iter() - .map(|component| component.render()) + .map(|component| component.into_any()) .collect() }), interactivity: Interactivity { @@ -252,6 +252,14 @@ impl Element for UniformList { } } +impl RenderOnce for UniformList { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + impl UniformList { pub fn with_width_from_item(mut self, item_index: Option) -> Self { self.item_to_measure_index = item_index.unwrap_or(0); @@ -286,14 +294,8 @@ impl UniformList { } } -impl InteractiveComponent for UniformList { +impl InteractiveElement for UniformList { fn interactivity(&mut self) -> &mut crate::Interactivity { &mut self.interactivity } } - -impl Component for UniformList { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 4423986e3d..a815c1f596 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -121,7 +121,7 @@ pub trait VisualContext: Context { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render; + V: 'static + Render; fn update_view( &mut self, @@ -134,7 +134,7 @@ pub trait VisualContext: Context { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: Render; + V: 'static + Render; fn focus_view(&mut self, view: &View) -> Self::Result<()> where diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 83bcc5c8c5..617972e3c0 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,5 +1,5 @@ use crate::{ - div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, + div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, }; use smallvec::SmallVec; @@ -64,7 +64,7 @@ pub struct Drag where R: Fn(&mut V, &mut ViewContext) -> E, V: 'static, - E: Component<()>, + E: Element<()>, { pub state: S, pub render_drag_handle: R, @@ -75,7 +75,7 @@ impl Drag where R: Fn(&mut V, &mut ViewContext) -> E, V: 'static, - E: Component<()>, + E: Element<()>, { pub fn new(state: S, render_drag_handle: R) -> Self { Drag { @@ -193,7 +193,7 @@ impl Deref for MouseExitEvent { #[derive(Debug, Clone, Default)] pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); -impl Render for ExternalPaths { +impl Render for ExternalPaths { type Element = Div; fn render(&mut self, _: &mut ViewContext) -> Self::Element { @@ -286,8 +286,8 @@ pub struct FocusEvent { #[cfg(test)] mod test { use crate::{ - self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding, - Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext, + self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke, + ParentElement, Stateful, TestAppContext, Render, VisualContext, }; struct TestView { @@ -298,7 +298,7 @@ mod test { actions!(TestAction); - impl Render for TestView { + impl Render for TestView { type Element = Stateful>; fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { diff --git a/crates/gpui2/src/prelude.rs b/crates/gpui2/src/prelude.rs index 7c2ad3f07f..50f48596bc 100644 --- a/crates/gpui2/src/prelude.rs +++ b/crates/gpui2/src/prelude.rs @@ -1,4 +1,5 @@ pub use crate::{ - BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent, - ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext, + BorrowAppContext, BorrowWindow, Component, Context, Element, FocusableElement, + InteractiveElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, + Styled, VisualContext, }; diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 51675e814c..c32bc70e4a 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,7 +1,8 @@ use crate::{ private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, - Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, - LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel, WindowContext, + Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, LayoutId, + Model, Pixels, Point, Render, RenderOnce, Size, ViewContext, VisualContext, WeakModel, + WindowContext, }; use anyhow::{Context, Result}; use std::{ @@ -9,14 +10,8 @@ use std::{ hash::{Hash, Hasher}, }; -pub trait Render: 'static + Sized { - type Element: Element + 'static; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element; -} - pub struct View { - pub(crate) model: Model, + pub model: Model, } impl Sealed for View {} @@ -64,13 +59,13 @@ impl View { self.model.read(cx) } - pub fn render_with(&self, component: C) -> RenderViewWith + pub fn render_with(&self, component: E) -> RenderViewWith where - C: 'static + Component, + E: 'static + Element, { RenderViewWith { view: self.clone(), - component: Some(component), + element: Some(component), } } @@ -104,12 +99,6 @@ impl PartialEq for View { impl Eq for View {} -impl Component for View { - fn render(self) -> AnyElement { - AnyElement::new(AnyView::from(self)) - } -} - pub struct WeakView { pub(crate) model: WeakModel, } @@ -206,13 +195,7 @@ impl AnyView { } } -impl Component for AnyView { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl From> for AnyView { +impl> From> for AnyView { fn from(value: View) -> Self { AnyView { model: value.model.into_any(), @@ -222,7 +205,48 @@ impl From> for AnyView { } } -impl Element for AnyView { +impl, ParentV: 'static> Element for View { + type State = Option>; + + fn element_id(&self) -> Option { + Some(self.model.entity_id.into()) + } + + fn layout( + &mut self, + _parent_view: &mut ParentV, + _state: Option, + cx: &mut ViewContext, + ) -> (LayoutId, Self::State) { + self.update(cx, |view, cx| { + let mut element = view.render(cx).into_any(); + let layout_id = element.layout(view, cx); + (layout_id, Some(element)) + }) + } + + fn paint( + self, + _: Bounds, + _parent: &mut ParentV, + element: &mut Self::State, + cx: &mut ViewContext, + ) { + self.update(cx, |view, cx| { + element.take().unwrap().paint(view, cx); + }); + } +} + +impl, ParentV: 'static> RenderOnce for View { + type Element = View; + + fn render_once(self) -> Self::Element { + self + } +} + +impl Element for AnyView { type State = Option>; fn element_id(&self) -> Option { @@ -231,9 +255,9 @@ impl Element for AnyView { fn layout( &mut self, - _view_state: &mut ParentViewState, + _view_state: &mut V, _element_state: Option, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> (LayoutId, Self::State) { let (layout_id, rendered_element) = (self.layout)(self, cx); (layout_id, Some(rendered_element)) @@ -242,14 +266,22 @@ impl Element for AnyView { fn paint( mut self, _bounds: Bounds, - _view_state: &mut ParentViewState, + _view_state: &mut V, rendered_element: &mut Self::State, - cx: &mut ViewContext, + cx: &mut ViewContext, ) { (self.paint)(&mut self, rendered_element.take().unwrap(), cx) } } +impl RenderOnce for AnyView { + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + pub struct AnyWeakView { model: AnyWeakModel, layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), @@ -267,7 +299,7 @@ impl AnyWeakView { } } -impl From> for AnyWeakView { +impl> From> for AnyWeakView { fn from(view: WeakView) -> Self { Self { model: view.model.into(), @@ -277,10 +309,10 @@ impl From> for AnyWeakView { } } -impl Render for T +impl Render for F where - T: 'static + FnMut(&mut WindowContext) -> E, - E: 'static + Send + Element, + F: 'static + FnMut(&mut WindowContext) -> E, + E: 'static + Send + Element, { type Element = E; @@ -289,29 +321,18 @@ where } } -pub struct RenderViewWith { +pub struct RenderViewWith { view: View, - component: Option, + element: Option, } -impl Component for RenderViewWith +impl Element for RenderViewWith where - C: 'static + Component, - ParentViewState: 'static, - ViewState: 'static, + E: 'static + Element, + ParentV: 'static, + V: 'static, { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl Element for RenderViewWith -where - C: 'static + Component, - ParentViewState: 'static, - ViewState: 'static, -{ - type State = Option>; + type State = Option>; fn element_id(&self) -> Option { Some(self.view.entity_id().into()) @@ -319,12 +340,12 @@ where fn layout( &mut self, - _: &mut ParentViewState, + _: &mut ParentV, _: Option, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> (LayoutId, Self::State) { self.view.update(cx, |view, cx| { - let mut element = self.component.take().unwrap().render(); + let mut element = self.element.take().unwrap().into_any(); let layout_id = element.layout(view, cx); (layout_id, Some(element)) }) @@ -333,20 +354,33 @@ where fn paint( self, _: Bounds, - _: &mut ParentViewState, + _: &mut ParentV, element: &mut Self::State, - cx: &mut ViewContext, + cx: &mut ViewContext, ) { self.view .update(cx, |view, cx| element.take().unwrap().paint(view, cx)) } } +impl RenderOnce for RenderViewWith +where + E: 'static + Element, + V: 'static, + ParentV: 'static, +{ + type Element = Self; + + fn render_once(self) -> Self::Element { + self + } +} + mod any_view { use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; use std::any::Any; - pub(crate) fn layout( + pub(crate) fn layout>( view: &AnyView, cx: &mut WindowContext, ) -> (LayoutId, Box) { @@ -360,7 +394,11 @@ mod any_view { }) } - pub(crate) fn paint(view: &AnyView, element: Box, cx: &mut WindowContext) { + pub(crate) fn paint>( + view: &AnyView, + element: Box, + cx: &mut WindowContext, + ) { cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); let element = element.downcast::>().unwrap(); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index a9a2c60acb..1b3b37c290 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -6,8 +6,8 @@ use crate::{ InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, - PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, - RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, + PolychromeSprite, PromptLevel, Quad, RenderGlyphParams, RenderImageParams, RenderSvgParams, + Render, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; @@ -187,13 +187,13 @@ impl Drop for FocusHandle { /// FocusableView allows users of your view to easily /// focus it (using cx.focus_view(view)) -pub trait FocusableView: Render { +pub trait FocusableView: 'static + Render { fn focus_handle(&self, cx: &AppContext) -> FocusHandle; } /// ManagedView is a view (like a Modal, Popover, Menu, etc.) /// where the lifecycle of the view is handled by another view. -pub trait ManagedView: Render { +pub trait ManagedView: 'static + Render { fn focus_handle(&self, cx: &AppContext) -> FocusHandle; } @@ -1525,7 +1525,7 @@ impl VisualContext for WindowContext<'_> { build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { let slot = self.app.entities.reserve(); let view = View { @@ -1564,7 +1564,7 @@ impl VisualContext for WindowContext<'_> { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: Render, + V: 'static + Render, { let slot = self.app.entities.reserve(); let view = View { @@ -2326,7 +2326,7 @@ impl Context for ViewContext<'_, V> { } impl VisualContext for ViewContext<'_, V> { - fn build_view( + fn build_view + 'static>( &mut self, build_view_state: impl FnOnce(&mut ViewContext<'_, W>) -> W, ) -> Self::Result> { @@ -2346,7 +2346,7 @@ impl VisualContext for ViewContext<'_, V> { build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W, ) -> Self::Result> where - W: Render, + W: 'static + Render, { self.window_cx.replace_root_view(build_view) } @@ -2387,7 +2387,7 @@ pub struct WindowHandle { state_type: PhantomData, } -impl WindowHandle { +impl> WindowHandle { pub fn new(id: WindowId) -> Self { WindowHandle { any_handle: AnyWindowHandle { diff --git a/crates/gpui2_macros/src/derive_element.rs b/crates/gpui2_macros/src/derive_element.rs deleted file mode 100644 index e8c698c1fe..0000000000 --- a/crates/gpui2_macros/src/derive_element.rs +++ /dev/null @@ -1,75 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput, GenericParam}; - -pub fn derive_element(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let type_name = ast.ident; - - let mut view_state_ty = quote! { V }; - - for param in &ast.generics.params { - if let GenericParam::Type(type_param) = param { - let type_ident = &type_param.ident; - view_state_ty = quote! {#type_ident}; - break; - } - } - - let attrs = &ast.attrs; - for attr in attrs { - if attr.path.is_ident("element") { - match attr.parse_meta() { - Ok(syn::Meta::List(i)) => { - for nested_meta in i.nested { - if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = nested_meta { - if nv.path.is_ident("view_state") { - if let syn::Lit::Str(lit_str) = nv.lit { - view_state_ty = lit_str.value().parse().unwrap(); - } - } - } - } - } - _ => (), - } - } - } - - let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); - - let gen = quote! { - impl #impl_generics gpui::Element<#view_state_ty> for #type_name #ty_generics - #where_clause - { - type State = Option>; - - fn element_id(&self) -> Option { - None - } - - fn layout( - &mut self, - view_state: &mut #view_state_ty, - _element_state: Option, - cx: &mut gpui::ViewContext<#view_state_ty>, - ) -> (gpui::LayoutId, Self::State) { - let mut element = self.render(view_state, cx).into_any(); - let layout_id = element.layout(view_state, cx); - (layout_id, Some(element)) - } - - fn paint( - self, - _bounds: gpui::Bounds, - view_state: &mut #view_state_ty, - rendered_element: &mut Self::State, - cx: &mut gpui::ViewContext<#view_state_ty>, - ) { - rendered_element.take().unwrap().paint(view_state, cx) - } - } - }; - - gen.into() -} diff --git a/crates/gpui2_macros/src/derive_render_once.rs b/crates/gpui2_macros/src/derive_render_once.rs new file mode 100644 index 0000000000..ee01e22358 --- /dev/null +++ b/crates/gpui2_macros/src/derive_render_once.rs @@ -0,0 +1,64 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, parse_quote, DeriveInput}; + +pub fn derive_render_once(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_name = &ast.ident; + + let mut trait_generics = ast.generics.clone(); + let view_type = if let Some(view_type) = specified_view_type(&ast) { + quote! { #view_type } + } else { + if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| { + if let syn::GenericParam::Type(type_param) = param { + Some(type_param.ident.clone()) + } else { + None + } + }) { + quote! { #first_type_param } + } else { + trait_generics.params.push(parse_quote! { V: 'static }); + quote! { V } + } + }; + + let (impl_generics, _, where_clause) = trait_generics.split_for_impl(); + let (_, type_generics, _) = ast.generics.split_for_impl(); + + let gen = quote! { + impl #impl_generics gpui::RenderOnce<#view_type> for #type_name #type_generics + #where_clause + { + type Element = gpui::CompositeElement<#view_type, Self>; + + fn render_once(self) -> Self::Element { + gpui::CompositeElement::new(self) + } + } + }; + + if type_name == "Avatar" { + println!("{gen}"); + } + + gen.into() +} + +fn specified_view_type(ast: &DeriveInput) -> Option { + ast.attrs.iter().find_map(|attr| { + if attr.path.is_ident("view") { + if let Ok(syn::Meta::NameValue(meta_name_value)) = attr.parse_meta() { + if let syn::Lit::Str(lit_str) = meta_name_value.lit { + return Some( + lit_str + .parse::() + .expect("Failed to parse view_type"), + ); + } + } + } + None + }) +} diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index a611727066..6dd817e280 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -1,6 +1,6 @@ mod action; mod derive_component; -mod derive_element; +mod derive_render_once; mod register_action; mod style_helpers; mod test; @@ -22,9 +22,9 @@ pub fn derive_component(input: TokenStream) -> TokenStream { derive_component::derive_component(input) } -#[proc_macro_derive(Element, attributes(element))] -pub fn derive_element(input: TokenStream) -> TokenStream { - derive_element::derive_element(input) +#[proc_macro_derive(RenderOnce, attributes(view))] +pub fn derive_render_once(input: TokenStream) -> TokenStream { + derive_render_once::derive_render_once(input) } #[proc_macro] diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 7a455fe8ce..46ad7a7a41 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -10,9 +10,9 @@ use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView, - InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, - Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + InteractiveElement, Model, MouseButton, ParentElement, Pixels, Point, PromptLevel, Render, + Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, ViewContext, + VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index 6fc76ab907..94c9d0d51f 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,4 +1,6 @@ -use gpui::{div, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext}; +use gpui::{ + div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext, +}; pub struct TextStory; diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 02165db0cd..4916f192b1 100644 --- a/crates/storybook2/src/stories/z_index.rs +++ b/crates/storybook2/src/stories/z_index.rs @@ -79,7 +79,7 @@ trait Styles: Styled + Sized { impl Styles for Div {} -#[derive(Component)] +// #[derive(RenderOnce)] struct ZIndexExample { z_index: u32, } diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index 944cd912be..6321e61e35 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/crates/terminal_view2/src/terminal_panel.rs @@ -4,7 +4,7 @@ use crate::TerminalView; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter, - FocusHandle, FocusableView, ParentComponent, Render, Subscription, Task, View, ViewContext, + FocusHandle, FocusableView, ParentElement, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use project::Fs; diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index b6ab7e86b9..8f5aeb5630 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -10,10 +10,9 @@ pub mod terminal_panel; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div, - EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, - InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton, - ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, - WeakView, + EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, + InputHandler, InteractiveElement, KeyDownEvent, Keystroke, Model, MouseButton, ParentElement, + Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use language::Bias; use persistence::TERMINAL_DB; diff --git a/crates/theme2/src/story.rs b/crates/theme2/src/story.rs index 9cf78cf943..e0c802fcc7 100644 --- a/crates/theme2/src/story.rs +++ b/crates/theme2/src/story.rs @@ -1,4 +1,4 @@ -use gpui::{div, Component, Div, Element, ParentComponent, SharedString, Styled, ViewContext}; +use gpui::{div, Div, Element, ParentElement, SharedString, Styled, ViewContext}; use crate::ActiveTheme; diff --git a/crates/theme2/src/styles/players.rs b/crates/theme2/src/styles/players.rs index dfb0a6ff4e..2eba4e8275 100644 --- a/crates/theme2/src/styles/players.rs +++ b/crates/theme2/src/styles/players.rs @@ -143,11 +143,11 @@ use crate::{amber, blue, jade, lime, orange, pink, purple, red}; mod stories { use super::*; use crate::{ActiveTheme, Story}; - use gpui::{div, img, px, Div, ParentComponent, Render, Styled, ViewContext}; + use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext}; pub struct PlayerStory; - impl Render for PlayerStory { + impl Render for PlayerStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 95e0653d36..da76a95cfa 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,26 +1,16 @@ use crate::prelude::*; -use gpui::img; +use gpui::{img, Img, RenderOnce}; -#[derive(Element)] +#[derive(RenderOnce)] pub struct Avatar { src: SharedString, shape: Shape, } -impl Avatar { - pub fn new(src: impl Into) -> Self { - Self { - src: src.into(), - shape: Shape::Circle, - } - } +impl Component for Avatar { + type Rendered = Img; - pub fn shape(mut self, shape: Shape) -> Self { - self.shape = shape; - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> Self::Rendered { let mut img = img(); if self.shape == Shape::Circle { @@ -36,6 +26,20 @@ impl Avatar { } } +impl Avatar { + pub fn new(src: impl Into) -> Self { + Self { + src: src.into(), + shape: Shape::Circle, + } + } + + pub fn shape(mut self, shape: Shape) -> Self { + self.shape = shape; + self + } +} + #[cfg(feature = "stories")] pub use stories::*; @@ -47,7 +51,7 @@ mod stories { pub struct AvatarStory; - impl Render for AvatarStory { + impl Render for AvatarStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index b2c6893793..f22727ae64 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,6 +1,9 @@ use std::sync::Arc; -use gpui::{div, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext}; +use gpui::{ + div, DefiniteLength, Div, Hsla, MouseButton, RenderOnce, Stateful, StatefulInteractiveElement, + WindowContext, +}; use crate::prelude::*; use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor}; @@ -76,7 +79,7 @@ impl Default for ButtonHandlers { } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct Button { disabled: bool, handlers: ButtonHandlers, @@ -88,6 +91,58 @@ pub struct Button { color: Option, } +impl Component for Button { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let _view: &mut V = view; + let (icon_color, label_color) = match (self.disabled, self.color) { + (true, _) => (TextColor::Disabled, TextColor::Disabled), + (_, None) => (TextColor::Default, TextColor::Default), + (_, Some(color)) => (TextColor::from(color), color), + }; + + let mut button = h_stack() + .id(SharedString::from(format!("{}", self.label))) + .relative() + .p_1() + .text_ui() + .rounded_md() + .bg(self.variant.bg_color(cx)) + .cursor_pointer() + .hover(|style| style.bg(self.variant.bg_color_hover(cx))) + .active(|style| style.bg(self.variant.bg_color_active(cx))); + + match (self.icon, self.icon_position) { + (Some(_), Some(IconPosition::Left)) => { + button = button + .gap_1() + .child(self.render_label(label_color)) + .children(self.render_icon(icon_color)) + } + (Some(_), Some(IconPosition::Right)) => { + button = button + .gap_1() + .children(self.render_icon(icon_color)) + .child(self.render_label(label_color)) + } + (_, _) => button = button.child(self.render_label(label_color)), + } + + if let Some(width) = self.width { + button = button.w(width).justify_center(); + } + + if let Some(click_handler) = self.handlers.click.clone() { + button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| { + click_handler(state, cx); + }); + } + + button + } +} + impl Button { pub fn new(label: impl Into) -> Self { Self { @@ -212,25 +267,29 @@ impl Button { } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct ButtonGroup { buttons: Vec>, } +impl Component for ButtonGroup { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let mut group = h_stack(); + + for button in self.buttons.into_iter() { + group = group.child(button.render(view, cx)); + } + + group + } +} + impl ButtonGroup { pub fn new(buttons: Vec>) -> Self { Self { buttons } } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - let mut el = h_stack().text_ui(); - - for button in self.buttons { - el = el.child(button.render(_view, cx)); - } - - el - } } #[cfg(feature = "stories")] @@ -245,7 +304,7 @@ mod stories { pub struct ButtonStory; - impl Render for ButtonStory { + impl Render for ButtonStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index 0b3141a9f3..3e1d4ff669 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -1,4 +1,4 @@ -use gpui::{div, prelude::*, Component, Element, ElementId, Styled, ViewContext}; +use gpui::{div, prelude::*, Div, Element, ElementId, RenderOnce, Stateful, Styled, ViewContext}; use std::sync::Arc; use theme2::ActiveTheme; @@ -11,7 +11,7 @@ pub type CheckHandler = Arc) + /// Checkboxes are used for multiple choices, not for mutually exclusive choices. /// Each checkbox works independently from other checkboxes in the list, /// therefore checking an additional box does not affect any other selections. -#[derive(Component)] +#[derive(RenderOnce)] pub struct Checkbox { id: ElementId, checked: Selection, @@ -19,6 +19,130 @@ pub struct Checkbox { on_click: Option>, } +impl Component for Checkbox { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let group_id = format!("checkbox_group_{:?}", self.id); + + let icon = match self.checked { + // When selected, we show a checkmark. + Selection::Selected => { + Some( + IconElement::new(Icon::Check) + .size(crate::IconSize::Small) + .color( + // If the checkbox is disabled we change the color of the icon. + if self.disabled { + TextColor::Disabled + } else { + TextColor::Selected + }, + ), + ) + } + // In an indeterminate state, we show a dash. + Selection::Indeterminate => { + Some( + IconElement::new(Icon::Dash) + .size(crate::IconSize::Small) + .color( + // If the checkbox is disabled we change the color of the icon. + if self.disabled { + TextColor::Disabled + } else { + TextColor::Selected + }, + ), + ) + } + // When unselected, we show nothing. + Selection::Unselected => None, + }; + + // A checkbox could be in an indeterminate state, + // for example the indeterminate state could represent: + // - a group of options of which only some are selected + // - an enabled option that is no longer available + // - a previously agreed to license that has been updated + // + // For the sake of styles we treat the indeterminate state as selected, + // but it's icon will be different. + let selected = + self.checked == Selection::Selected || self.checked == Selection::Indeterminate; + + // We could use something like this to make the checkbox background when selected: + // + // ~~~rust + // ... + // .when(selected, |this| { + // this.bg(cx.theme().colors().element_selected) + // }) + // ~~~ + // + // But we use a match instead here because the checkbox might be disabled, + // and it could be disabled _while_ it is selected, as well as while it is not selected. + let (bg_color, border_color) = match (self.disabled, selected) { + (true, _) => ( + cx.theme().colors().ghost_element_disabled, + cx.theme().colors().border_disabled, + ), + (false, true) => ( + cx.theme().colors().element_selected, + cx.theme().colors().border, + ), + (false, false) => ( + cx.theme().colors().element_background, + cx.theme().colors().border, + ), + }; + + div() + .id(self.id) + // Rather than adding `px_1()` to add some space around the checkbox, + // we use a larger parent element to create a slightly larger + // click area for the checkbox. + .size_5() + // Because we've enlarged the click area, we need to create a + // `group` to pass down interactivity events to the checkbox. + .group(group_id.clone()) + .child( + div() + .flex() + // This prevent the flex element from growing + // or shrinking in response to any size changes + .flex_none() + // The combo of `justify_center()` and `items_center()` + // is used frequently to center elements in a flex container. + // + // We use this to center the icon in the checkbox. + .justify_center() + .items_center() + .m_1() + .size_4() + .rounded_sm() + .bg(bg_color) + .border() + .border_color(border_color) + // We only want the interactivity states to fire when we + // are in a checkbox that isn't disabled. + .when(!self.disabled, |this| { + // Here instead of `hover()` we use `group_hover()` + // to pass it the group id. + this.group_hover(group_id.clone(), |el| { + el.bg(cx.theme().colors().element_hover) + }) + }) + .children(icon), + ) + .when_some( + self.on_click.filter(|_| !self.disabled), + |this, on_click| { + this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx)) + }, + ) + } +} impl Checkbox { pub fn new(id: impl Into, checked: Selection) -> Self { Self { @@ -175,7 +299,7 @@ mod stories { pub struct CheckboxStory; - impl Render for CheckboxStory { + impl Render for CheckboxStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index c6d2382f56..41675e6bcd 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -5,7 +5,8 @@ use crate::prelude::*; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; use gpui::{ overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div, - FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, + FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, + RenderOnce, View, }; pub struct ContextMenu { @@ -52,7 +53,7 @@ impl ContextMenu { } } -impl Render for ContextMenu { +impl Render for ContextMenu { type Element = Div; // todo!() fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -96,8 +97,8 @@ impl MenuHandle { self } - pub fn child>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self { - self.child_builder = Some(Box::new(|b| f(b).render())); + pub fn child>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self { + self.child_builder = Some(Box::new(|b| f(b).render_once().into_any())); self } @@ -160,9 +161,9 @@ impl Element for MenuHandle { } overlay = overlay.position(*position.borrow()); - let mut view = overlay.child(menu.clone()).render(); - menu_layout_id = Some(view.layout(view_state, cx)); - view + let mut element = overlay.child(menu.clone()).into_any(); + menu_layout_id = Some(element.layout(view_state, cx)); + element }); let mut child_element = self @@ -247,9 +248,11 @@ impl Element for MenuHandle { } } -impl Component for MenuHandle { - fn render(self) -> AnyElement { - AnyElement::new(self) +impl RenderOnce for MenuHandle { + type Element = Self; + + fn render_once(self) -> Self::Element { + self } } @@ -275,7 +278,7 @@ mod stories { pub struct ContextMenuStory; - impl Render for ContextMenuStory { + impl Render for ContextMenuStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -302,7 +305,6 @@ mod stories { } else { "RIGHT CLICK ME" }) - .render() }) .menu(move |_, cx| build_menu(cx, "top left")), ) @@ -315,7 +317,6 @@ mod stories { } else { "RIGHT CLICK ME" }) - .render() }) .anchor(AnchorCorner::BottomLeft) .attach(AnchorCorner::TopLeft) @@ -336,7 +337,6 @@ mod stories { } else { "RIGHT CLICK ME" }) - .render() }) .anchor(AnchorCorner::TopRight) .menu(move |_, cx| build_menu(cx, "top right")), @@ -350,7 +350,6 @@ mod stories { } else { "RIGHT CLICK ME" }) - .render() }) .anchor(AnchorCorner::BottomRight) .attach(AnchorCorner::TopRight) diff --git a/crates/ui2/src/components/details.rs b/crates/ui2/src/components/details.rs index 41b3e45a95..95750d5b47 100644 --- a/crates/ui2/src/components/details.rs +++ b/crates/ui2/src/components/details.rs @@ -1,13 +1,29 @@ use crate::prelude::*; use crate::{v_stack, ButtonGroup}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct Details { text: &'static str, meta: Option<&'static str>, actions: Option>, } +impl Component for Details { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + v_stack() + .p_1() + .gap_0p5() + .text_ui_sm() + .text_color(cx.theme().colors().text) + .size_full() + .child(self.text) + .children(self.meta.map(|m| m)) + .children(self.actions.map(|a| a)) + } +} + impl Details { pub fn new(text: &'static str) -> Self { Self { @@ -26,20 +42,9 @@ impl Details { self.actions = Some(actions); self } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - v_stack() - .p_1() - .gap_0p5() - .text_ui_sm() - .text_color(cx.theme().colors().text) - .size_full() - .child(self.text) - .children(self.meta.map(|m| m)) - .children(self.actions.map(|a| a)) - } } +use gpui::{Div, RenderOnce}; #[cfg(feature = "stories")] pub use stories::*; @@ -51,7 +56,7 @@ mod stories { pub struct DetailsStory; - impl Render for DetailsStory { + impl Render for DetailsStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/divider.rs b/crates/ui2/src/components/divider.rs index 7025a99a9f..a651ab08af 100644 --- a/crates/ui2/src/components/divider.rs +++ b/crates/ui2/src/components/divider.rs @@ -1,3 +1,5 @@ +use gpui::RenderOnce; + use crate::prelude::*; enum DividerDirection { @@ -5,7 +7,7 @@ enum DividerDirection { Vertical, } -#[derive(Component)] +// #[derive(RenderOnce)] pub struct Divider { direction: DividerDirection, inset: bool, diff --git a/crates/ui2/src/components/facepile.rs b/crates/ui2/src/components/facepile.rs index a3ec7db7b1..8bb3c9b2e0 100644 --- a/crates/ui2/src/components/facepile.rs +++ b/crates/ui2/src/components/facepile.rs @@ -1,19 +1,15 @@ use crate::prelude::*; use crate::{Avatar, Player}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct Facepile { players: Vec, } -impl Facepile { - pub fn new>(players: P) -> Self { - Self { - players: players.collect(), - } - } +impl Component for Facepile { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { let player_count = self.players.len(); let player_list = self.players.iter().enumerate().map(|(ix, player)| { let isnt_last = ix < player_count - 1; @@ -26,6 +22,15 @@ impl Facepile { } } +impl Facepile { + pub fn new>(players: P) -> Self { + Self { + players: players.collect(), + } + } +} + +use gpui::{Div, RenderOnce}; #[cfg(feature = "stories")] pub use stories::*; @@ -37,7 +42,7 @@ mod stories { pub struct FacepileStory; - impl Render for FacepileStory { + impl Render for FacepileStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 7dc9385a4f..0bf2828ad6 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -1,4 +1,4 @@ -use gpui::{rems, svg}; +use gpui::{rems, svg, RenderOnce, Svg}; use strum::EnumIter; use crate::prelude::*; @@ -129,13 +129,30 @@ impl Icon { } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct IconElement { path: SharedString, color: TextColor, size: IconSize, } +impl Component for IconElement { + type Rendered = Svg; + + fn render(self, _view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let svg_size = match self.size { + IconSize::Small => rems(0.75), + IconSize::Medium => rems(0.9375), + }; + + svg() + .size(svg_size) + .flex_none() + .path(self.path) + .text_color(self.color.color(cx)) + } +} + impl IconElement { pub fn new(icon: Icon) -> Self { Self { @@ -191,7 +208,7 @@ mod stories { pub struct IconStory; - impl Render for IconStory { + impl Render for IconStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 5bb26cdb91..ea015e95a6 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,5 +1,5 @@ use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement}; -use gpui::{prelude::*, Action, AnyView, MouseButton}; +use gpui::{prelude::*, Action, AnyView, Div, MouseButton, Stateful}; use std::sync::Arc; struct IconButtonHandlers { @@ -12,7 +12,7 @@ impl Default for IconButtonHandlers { } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct IconButton { id: ElementId, icon: Icon, @@ -24,6 +24,61 @@ pub struct IconButton { handlers: IconButtonHandlers, } +impl Component for IconButton { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let icon_color = match (self.state, self.color) { + (InteractionState::Disabled, _) => TextColor::Disabled, + (InteractionState::Active, _) => TextColor::Selected, + _ => self.color, + }; + + let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant { + ButtonVariant::Filled => ( + cx.theme().colors().element_background, + cx.theme().colors().element_hover, + cx.theme().colors().element_active, + ), + ButtonVariant::Ghost => ( + cx.theme().colors().ghost_element_background, + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, + ), + }; + + if self.selected { + bg_color = bg_hover_color; + } + + let mut button = h_stack() + .id(self.id.clone()) + .justify_center() + .rounded_md() + .p_1() + .bg(bg_color) + .cursor_pointer() + .hover(|style| style.bg(bg_hover_color)) + .active(|style| style.bg(bg_active_color)) + .child(IconElement::new(self.icon).color(icon_color)); + + if let Some(click_handler) = self.handlers.click.clone() { + button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| { + cx.stop_propagation(); + click_handler(state, cx); + }) + } + + if let Some(tooltip) = self.tooltip { + if !self.selected { + button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) + } + } + + button + } +} + impl IconButton { pub fn new(id: impl Into, icon: Icon) -> Self { Self { @@ -79,55 +134,4 @@ impl IconButton { pub fn action(self, action: Box) -> Self { self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) } - - fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - let icon_color = match (self.state, self.color) { - (InteractionState::Disabled, _) => TextColor::Disabled, - (InteractionState::Active, _) => TextColor::Selected, - _ => self.color, - }; - - let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant { - ButtonVariant::Filled => ( - cx.theme().colors().element_background, - cx.theme().colors().element_hover, - cx.theme().colors().element_active, - ), - ButtonVariant::Ghost => ( - cx.theme().colors().ghost_element_background, - cx.theme().colors().ghost_element_hover, - cx.theme().colors().ghost_element_active, - ), - }; - - if self.selected { - bg_color = bg_hover_color; - } - - let mut button = h_stack() - .id(self.id.clone()) - .justify_center() - .rounded_md() - .p_1() - .bg(bg_color) - .cursor_pointer() - .hover(|style| style.bg(bg_hover_color)) - .active(|style| style.bg(bg_active_color)) - .child(IconElement::new(self.icon).color(icon_color)); - - if let Some(click_handler) = self.handlers.click.clone() { - button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| { - cx.stop_propagation(); - click_handler(state, cx); - }) - } - - if let Some(tooltip) = self.tooltip.take() { - if !self.selected { - button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) - } - } - - button - } } diff --git a/crates/ui2/src/components/indicator.rs b/crates/ui2/src/components/indicator.rs index 02760852b1..33c0307cd4 100644 --- a/crates/ui2/src/components/indicator.rs +++ b/crates/ui2/src/components/indicator.rs @@ -1,10 +1,24 @@ -use gpui::px; - use crate::prelude::*; +use gpui::{px, Div, RenderOnce}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct UnreadIndicator; +impl Component for UnreadIndicator { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div() + .rounded_full() + .border_2() + .border_color(cx.theme().colors().surface_background) + .w(px(9.0)) + .h(px(9.0)) + .z_index(2) + .bg(cx.theme().status().info) + } +} + impl UnreadIndicator { pub fn new() -> Self { Self diff --git a/crates/ui2/src/components/input.rs b/crates/ui2/src/components/input.rs index 1389d83ce2..ea0312ab82 100644 --- a/crates/ui2/src/components/input.rs +++ b/crates/ui2/src/components/input.rs @@ -1,5 +1,5 @@ use crate::{prelude::*, Label}; -use gpui::prelude::*; +use gpui::{prelude::*, Div, RenderOnce, Stateful}; #[derive(Default, PartialEq)] pub enum InputVariant { @@ -8,7 +8,7 @@ pub enum InputVariant { Filled, } -#[derive(Component)] +#[derive(RenderOnce)] pub struct Input { placeholder: SharedString, value: String, @@ -18,6 +18,57 @@ pub struct Input { is_active: bool, } +impl Component for Input { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let (input_bg, input_hover_bg, input_active_bg) = match self.variant { + InputVariant::Ghost => ( + cx.theme().colors().ghost_element_background, + cx.theme().colors().ghost_element_hover, + cx.theme().colors().ghost_element_active, + ), + InputVariant::Filled => ( + cx.theme().colors().element_background, + cx.theme().colors().element_hover, + cx.theme().colors().element_active, + ), + }; + + let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled { + TextColor::Disabled + } else { + TextColor::Placeholder + }); + + let label = Label::new(self.value.clone()).color(if self.disabled { + TextColor::Disabled + } else { + TextColor::Default + }); + + div() + .id("input") + .h_7() + .w_full() + .px_2() + .border() + .border_color(cx.theme().styles.system.transparent) + .bg(input_bg) + .hover(|style| style.bg(input_hover_bg)) + .active(|style| style.bg(input_active_bg)) + .flex() + .items_center() + .child(div().flex().items_center().text_ui_sm().map(move |this| { + if self.value.is_empty() { + this.child(placeholder_label) + } else { + this.child(label) + } + })) + } +} + impl Input { pub fn new(placeholder: impl Into) -> Self { Self { @@ -54,53 +105,6 @@ impl Input { self.is_active = is_active; self } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - let (input_bg, input_hover_bg, input_active_bg) = match self.variant { - InputVariant::Ghost => ( - cx.theme().colors().ghost_element_background, - cx.theme().colors().ghost_element_hover, - cx.theme().colors().ghost_element_active, - ), - InputVariant::Filled => ( - cx.theme().colors().element_background, - cx.theme().colors().element_hover, - cx.theme().colors().element_active, - ), - }; - - let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled { - TextColor::Disabled - } else { - TextColor::Placeholder - }); - - let label = Label::new(self.value.clone()).color(if self.disabled { - TextColor::Disabled - } else { - TextColor::Default - }); - - div() - .id("input") - .h_7() - .w_full() - .px_2() - .border() - .border_color(cx.theme().styles.system.transparent) - .bg(input_bg) - .hover(|style| style.bg(input_hover_bg)) - .active(|style| style.bg(input_active_bg)) - .flex() - .items_center() - .child(div().flex().items_center().text_ui_sm().map(|this| { - if self.value.is_empty() { - this.child(placeholder_label) - } else { - this.child(label) - } - })) - } } #[cfg(feature = "stories")] @@ -114,7 +118,7 @@ mod stories { pub struct InputStory; - impl Render for InputStory { + impl Render for InputStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 608e3ab0b8..ce7c408fb2 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,9 +1,8 @@ -use gpui::Action; +use crate::prelude::*; +use gpui::{Action, Div, RenderOnce}; use strum::EnumIter; -use crate::prelude::*; - -#[derive(Component, Clone)] +#[derive(RenderOnce, Clone)] pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. @@ -12,19 +11,10 @@ pub struct KeyBinding { key_binding: gpui::KeyBinding, } -impl KeyBinding { - pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { - // todo! this last is arbitrary, we want to prefer users key bindings over defaults, - // and vim over normal (in vim mode), etc. - let key_binding = cx.bindings_for_action(action).last().cloned()?; - Some(Self::new(key_binding)) - } +impl Component for KeyBinding { + type Rendered = Div; - pub fn new(key_binding: gpui::KeyBinding) -> Self { - Self { key_binding } - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div() .flex() .gap_2() @@ -42,17 +32,29 @@ impl KeyBinding { } } -#[derive(Component)] +impl KeyBinding { + pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { + // todo! this last is arbitrary, we want to prefer users key bindings over defaults, + // and vim over normal (in vim mode), etc. + let key_binding = cx.bindings_for_action(action).last().cloned()?; + Some(Self::new(key_binding)) + } + + pub fn new(key_binding: gpui::KeyBinding) -> Self { + Self { key_binding } + } +} + +#[derive(RenderOnce)] pub struct Key { key: SharedString, } -impl Key { - pub fn new(key: impl Into) -> Self { - Self { key: key.into() } - } +impl Component for Key { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let _view: &mut V = view; div() .px_2() .py_0() @@ -64,6 +66,12 @@ impl Key { } } +impl Key { + pub fn new(key: impl Into) -> Self { + Self { key: key.into() } + } +} + // NOTE: The order the modifier keys appear in this enum impacts the order in // which they are rendered in the UI. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] @@ -92,7 +100,7 @@ mod stories { gpui::KeyBinding::new(key, NoAction {}, None) } - impl Render for KeybindingStory { + impl Render for KeybindingStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index c07c467000..7afb00a96e 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -1,7 +1,6 @@ -use gpui::{relative, Hsla, Text, TextRun, WindowContext}; - use crate::prelude::*; use crate::styled_ext::StyledExt; +use gpui::{relative, Div, Hsla, RenderOnce, StyledText, TextRun, WindowContext}; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] pub enum LabelSize { @@ -60,7 +59,7 @@ pub enum LineHeightStyle { UILabel, } -#[derive(Clone, Component)] +#[derive(Clone, RenderOnce)] pub struct Label { label: SharedString, size: LabelSize, @@ -69,6 +68,33 @@ pub struct Label { strikethrough: bool, } +impl Component for Label { + type Rendered = Div; + + fn render(self, _view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div() + .when(self.strikethrough, |this| { + this.relative().child( + div() + .absolute() + .top_1_2() + .w_full() + .h_px() + .bg(TextColor::Hidden.color(cx)), + ) + }) + .map(|this| match self.size { + LabelSize::Default => this.text_ui(), + LabelSize::Small => this.text_ui_sm(), + }) + .when(self.line_height_style == LineHeightStyle::UILabel, |this| { + this.line_height(relative(1.)) + }) + .text_color(self.color.color(cx)) + .child(self.label.clone()) + } +} + impl Label { pub fn new(label: impl Into) -> Self { Self { @@ -99,32 +125,9 @@ impl Label { self.strikethrough = strikethrough; self } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - div() - .when(self.strikethrough, |this| { - this.relative().child( - div() - .absolute() - .top_1_2() - .w_full() - .h_px() - .bg(TextColor::Hidden.color(cx)), - ) - }) - .map(|this| match self.size { - LabelSize::Default => this.text_ui(), - LabelSize::Small => this.text_ui_sm(), - }) - .when(self.line_height_style == LineHeightStyle::UILabel, |this| { - this.line_height(relative(1.)) - }) - .text_color(self.color.color(cx)) - .child(self.label.clone()) - } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct HighlightedLabel { label: SharedString, size: LabelSize, @@ -133,35 +136,10 @@ pub struct HighlightedLabel { strikethrough: bool, } -impl HighlightedLabel { - /// shows a label with the given characters highlighted. - /// characters are identified by utf8 byte position. - pub fn new(label: impl Into, highlight_indices: Vec) -> Self { - Self { - label: label.into(), - size: LabelSize::Default, - color: TextColor::Default, - highlight_indices, - strikethrough: false, - } - } +impl Component for HighlightedLabel { + type Rendered = Div; - pub fn size(mut self, size: LabelSize) -> Self { - self.size = size; - self - } - - pub fn color(mut self, color: TextColor) -> Self { - self.color = color; - self - } - - pub fn set_strikethrough(mut self, strikethrough: bool) -> Self { - self.strikethrough = strikethrough; - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { let highlight_color = cx.theme().colors().text_accent; let mut text_style = cx.text_style().clone(); @@ -214,7 +192,36 @@ impl HighlightedLabel { LabelSize::Default => this.text_ui(), LabelSize::Small => this.text_ui_sm(), }) - .child(Text::styled(self.label, runs)) + .child(StyledText::new(self.label, runs)) + } +} + +impl HighlightedLabel { + /// shows a label with the given characters highlighted. + /// characters are identified by utf8 byte position. + pub fn new(label: impl Into, highlight_indices: Vec) -> Self { + Self { + label: label.into(), + size: LabelSize::Default, + color: TextColor::Default, + highlight_indices, + strikethrough: false, + } + } + + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + + pub fn color(mut self, color: TextColor) -> Self { + self.color = color; + self + } + + pub fn set_strikethrough(mut self, strikethrough: bool) -> Self { + self.strikethrough = strikethrough; + self } } @@ -235,7 +242,7 @@ mod stories { pub struct LabelStory; - impl Render for LabelStory { + impl Render for LabelStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 8756d2934c..b183c149fc 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,4 +1,4 @@ -use gpui::{div, Action}; +use gpui::{div, Action, Div, RenderOnce}; use crate::settings::user_settings; use crate::{ @@ -22,7 +22,7 @@ pub enum ListHeaderMeta { Text(Label), } -#[derive(Component)] +#[derive(RenderOnce)] pub struct ListHeader { label: SharedString, left_icon: Option, @@ -31,33 +31,10 @@ pub struct ListHeader { toggle: Toggle, } -impl ListHeader { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - left_icon: None, - meta: None, - variant: ListItemVariant::default(), - toggle: Toggle::NotToggleable, - } - } +impl Component for ListHeader { + type Rendered = Div; - pub fn toggle(mut self, toggle: Toggle) -> Self { - self.toggle = toggle; - self - } - - pub fn left_icon(mut self, left_icon: Option) -> Self { - self.left_icon = left_icon; - self - } - - pub fn meta(mut self, meta: Option) -> Self { - self.meta = meta; - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { let disclosure_control = disclosure_control(self.toggle); let meta = match self.meta { @@ -79,11 +56,6 @@ impl ListHeader { h_stack() .w_full() .bg(cx.theme().colors().surface_background) - // TODO: Add focus state - // .when(self.state == InteractionState::Focused, |this| { - // this.border() - // .border_color(cx.theme().colors().border_focused) - // }) .relative() .child( div() @@ -117,7 +89,94 @@ impl ListHeader { } } -#[derive(Component, Clone)] +impl ListHeader { + pub fn new(label: impl Into) -> Self { + Self { + label: label.into(), + left_icon: None, + meta: None, + variant: ListItemVariant::default(), + toggle: Toggle::NotToggleable, + } + } + + pub fn toggle(mut self, toggle: Toggle) -> Self { + self.toggle = toggle; + self + } + + pub fn left_icon(mut self, left_icon: Option) -> Self { + self.left_icon = left_icon; + self + } + + pub fn meta(mut self, meta: Option) -> Self { + self.meta = meta; + self + } + + // before_ship!("delete") + // fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + // let disclosure_control = disclosure_control(self.toggle); + + // let meta = match self.meta { + // Some(ListHeaderMeta::Tools(icons)) => div().child( + // h_stack() + // .gap_2() + // .items_center() + // .children(icons.into_iter().map(|i| { + // IconElement::new(i) + // .color(TextColor::Muted) + // .size(IconSize::Small) + // })), + // ), + // Some(ListHeaderMeta::Button(label)) => div().child(label), + // Some(ListHeaderMeta::Text(label)) => div().child(label), + // None => div(), + // }; + + // h_stack() + // .w_full() + // .bg(cx.theme().colors().surface_background) + // // TODO: Add focus state + // // .when(self.state == InteractionState::Focused, |this| { + // // this.border() + // // .border_color(cx.theme().colors().border_focused) + // // }) + // .relative() + // .child( + // div() + // .h_5() + // .when(self.variant == ListItemVariant::Inset, |this| this.px_2()) + // .flex() + // .flex_1() + // .items_center() + // .justify_between() + // .w_full() + // .gap_1() + // .child( + // h_stack() + // .gap_1() + // .child( + // div() + // .flex() + // .gap_1() + // .items_center() + // .children(self.left_icon.map(|i| { + // IconElement::new(i) + // .color(TextColor::Muted) + // .size(IconSize::Small) + // })) + // .child(Label::new(self.label.clone()).color(TextColor::Muted)), + // ) + // .child(disclosure_control), + // ) + // .child(meta), + // ) + // } +} + +#[derive(Clone)] pub struct ListSubHeader { label: SharedString, left_icon: Option, @@ -172,7 +231,7 @@ pub enum ListEntrySize { Medium, } -#[derive(Component, Clone)] +#[derive(RenderOnce, Clone)] pub enum ListItem { Entry(ListEntry), Separator(ListSeparator), @@ -197,15 +256,19 @@ impl From for ListItem { } } -impl ListItem { - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Element { +impl Component for ListItem { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { match self { ListItem::Entry(entry) => div().child(entry.render(view, cx)), ListItem::Separator(separator) => div().child(separator.render(view, cx)), ListItem::Header(header) => div().child(header.render(view, cx)), } } +} +impl ListItem { pub fn new(label: Label) -> Self { Self::Entry(ListEntry::new(label)) } @@ -219,7 +282,7 @@ impl ListItem { } } -#[derive(Component)] +// #[derive(RenderOnce)] pub struct ListEntry { disabled: bool, // TODO: Reintroduce this @@ -377,20 +440,24 @@ impl ListEntry { } } -#[derive(Clone, Component)] +#[derive(RenderOnce, Clone)] pub struct ListSeparator; impl ListSeparator { pub fn new() -> Self { Self } +} - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { +impl Component for ListSeparator { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div().h_px().w_full().bg(cx.theme().colors().border_variant) } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct List { items: Vec, /// Message to display when the list is empty @@ -400,6 +467,26 @@ pub struct List { toggle: Toggle, } +impl Component for List { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let list_content = match (self.items.is_empty(), self.toggle) { + (false, _) => div().children(self.items), + (true, Toggle::Toggled(false)) => div(), + (true, _) => { + div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted)) + } + }; + + v_stack() + .w_full() + .py_1() + .children(self.header.map(|header| header)) + .child(list_content) + } +} + impl List { pub fn new(items: Vec) -> Self { Self { diff --git a/crates/ui2/src/components/modal.rs b/crates/ui2/src/components/modal.rs index ac4cb4017f..da76a79b29 100644 --- a/crates/ui2/src/components/modal.rs +++ b/crates/ui2/src/components/modal.rs @@ -1,9 +1,9 @@ -use gpui::AnyElement; +use gpui::{AnyElement, Div, RenderOnce, Stateful}; use smallvec::SmallVec; use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct Modal { id: ElementId, title: Option, @@ -12,33 +12,11 @@ pub struct Modal { children: SmallVec<[AnyElement; 2]>, } -impl Modal { - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - title: None, - primary_action: None, - secondary_action: None, - children: SmallVec::new(), - } - } +impl Component for Modal { + type Rendered = Stateful>; - pub fn title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self - } - - pub fn primary_action(mut self, action: Button) -> Self { - self.primary_action = Some(action); - self - } - - pub fn secondary_action(mut self, action: Button) -> Self { - self.secondary_action = Some(action); - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let _view: &mut V = view; v_stack() .id(self.id.clone()) .w_96() @@ -74,7 +52,34 @@ impl Modal { } } -impl ParentComponent for Modal { +impl Modal { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + title: None, + primary_action: None, + secondary_action: None, + children: SmallVec::new(), + } + } + + pub fn title(mut self, title: impl Into) -> Self { + self.title = Some(title.into()); + self + } + + pub fn primary_action(mut self, action: Button) -> Self { + self.primary_action = Some(action); + self + } + + pub fn secondary_action(mut self, action: Button) -> Self { + self.secondary_action = Some(action); + self + } +} + +impl ParentElement for Modal { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } diff --git a/crates/ui2/src/components/notification_toast.rs b/crates/ui2/src/components/notification_toast.rs index ec0d2ac216..dd5165e49f 100644 --- a/crates/ui2/src/components/notification_toast.rs +++ b/crates/ui2/src/components/notification_toast.rs @@ -3,7 +3,7 @@ use gpui::rems; use crate::prelude::*; use crate::{h_stack, Icon}; -#[derive(Component)] +// #[derive(RenderOnce)] pub struct NotificationToast { label: SharedString, icon: Option, diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 4b6daea0f9..9870c9e086 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -1,7 +1,9 @@ use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label}; use gpui::prelude::*; +use gpui::Div; +use gpui::Stateful; -#[derive(Component)] +#[derive(RenderOnce)] pub struct Palette { id: ElementId, input_placeholder: SharedString, @@ -10,6 +12,70 @@ pub struct Palette { default_order: OrderMethod, } +impl Component for Palette { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + v_stack() + .id(self.id) + .w_96() + .rounded_lg() + .bg(cx.theme().colors().elevated_surface_background) + .border() + .border_color(cx.theme().colors().border) + .child( + v_stack() + .gap_px() + .child(v_stack().py_0p5().px_1().child( + div().px_2().py_0p5().child( + Label::new(self.input_placeholder).color(TextColor::Placeholder), + ), + )) + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().element_background), + ) + .child( + v_stack() + .id("items") + .py_0p5() + .px_1() + .grow() + .max_h_96() + .overflow_y_scroll() + .children( + vec![if self.items.is_empty() { + Some(h_stack().justify_between().px_2().py_1().child( + Label::new(self.empty_string).color(TextColor::Muted), + )) + } else { + None + }] + .into_iter() + .flatten(), + ) + .children(self.items.into_iter().enumerate().map(|(index, item)| { + h_stack() + .id(index) + .justify_between() + .px_2() + .py_0p5() + .rounded_lg() + .hover(|style| { + style.bg(cx.theme().colors().ghost_element_hover) + }) + .active(|style| { + style.bg(cx.theme().colors().ghost_element_active) + }) + .child(item) + })), + ), + ) + } +} + impl Palette { pub fn new(id: impl Into) -> Self { Self { @@ -41,76 +107,33 @@ impl Palette { self.default_order = default_order; self } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - v_stack() - .id(self.id.clone()) - .w_96() - .rounded_lg() - .bg(cx.theme().colors().elevated_surface_background) - .border() - .border_color(cx.theme().colors().border) - .child( - v_stack() - .gap_px() - .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child( - Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder), - ))) - .child( - div() - .h_px() - .w_full() - .bg(cx.theme().colors().element_background), - ) - .child( - v_stack() - .id("items") - .py_0p5() - .px_1() - .grow() - .max_h_96() - .overflow_y_scroll() - .children( - vec![if self.items.is_empty() { - Some( - h_stack().justify_between().px_2().py_1().child( - Label::new(self.empty_string.clone()) - .color(TextColor::Muted), - ), - ) - } else { - None - }] - .into_iter() - .flatten(), - ) - .children(self.items.into_iter().enumerate().map(|(index, item)| { - h_stack() - .id(index) - .justify_between() - .px_2() - .py_0p5() - .rounded_lg() - .hover(|style| { - style.bg(cx.theme().colors().ghost_element_hover) - }) - .active(|style| { - style.bg(cx.theme().colors().ghost_element_active) - }) - .child(item) - })), - ), - ) - } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct PaletteItem { pub label: SharedString, pub sublabel: Option, pub key_binding: Option, } +impl Component for PaletteItem { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div() + .flex() + .flex_row() + .grow() + .justify_between() + .child( + v_stack() + .child(Label::new(self.label)) + .children(self.sublabel.map(|sublabel| Label::new(sublabel))), + ) + .children(self.key_binding) + } +} + impl PaletteItem { pub fn new(label: impl Into) -> Self { Self { @@ -134,20 +157,6 @@ impl PaletteItem { self.key_binding = key_binding.into(); self } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - div() - .flex() - .flex_row() - .grow() - .justify_between() - .child( - v_stack() - .child(Label::new(self.label.clone())) - .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))), - ) - .children(self.key_binding) - } } use gpui::ElementId; @@ -164,7 +173,7 @@ mod stories { pub struct PaletteStory; - impl Render for PaletteStory { + impl Render for PaletteStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index 88aca620da..38d68ab3ea 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -1,4 +1,4 @@ -use gpui::{prelude::*, AbsoluteLength, AnyElement}; +use gpui::{prelude::*, AbsoluteLength, AnyElement, Div, RenderOnce, Stateful}; use smallvec::SmallVec; use crate::prelude::*; @@ -38,7 +38,7 @@ pub enum PanelSide { use std::collections::HashSet; -#[derive(Component)] +#[derive(RenderOnce)] pub struct Panel { id: ElementId, current_side: PanelSide, @@ -49,6 +49,30 @@ pub struct Panel { children: SmallVec<[AnyElement; 2]>, } +impl Component for Panel { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let current_size = self.width.unwrap_or(self.initial_width); + + v_stack() + .id(self.id.clone()) + .flex_initial() + .map(|this| match self.current_side { + PanelSide::Left | PanelSide::Right => this.h_full().w(current_size), + PanelSide::Bottom => this, + }) + .map(|this| match self.current_side { + PanelSide::Left => this.border_r(), + PanelSide::Right => this.border_l(), + PanelSide::Bottom => this.border_b().w_full().h(current_size), + }) + .bg(cx.theme().colors().surface_background) + .border_color(cx.theme().colors().border) + .children(self.children) + } +} + impl Panel { pub fn new(id: impl Into, cx: &mut WindowContext) -> Self { let settings = user_settings(cx); @@ -91,29 +115,9 @@ impl Panel { } self } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - let current_size = self.width.unwrap_or(self.initial_width); - - v_stack() - .id(self.id.clone()) - .flex_initial() - .map(|this| match self.current_side { - PanelSide::Left | PanelSide::Right => this.h_full().w(current_size), - PanelSide::Bottom => this, - }) - .map(|this| match self.current_side { - PanelSide::Left => this.border_r(), - PanelSide::Right => this.border_l(), - PanelSide::Bottom => this.border_b().w_full().h(current_size), - }) - .bg(cx.theme().colors().surface_background) - .border_color(cx.theme().colors().border) - .children(self.children) - } } -impl ParentComponent for Panel { +impl ParentElement for Panel { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } @@ -126,11 +130,11 @@ pub use stories::*; mod stories { use super::*; use crate::{Label, Story}; - use gpui::{Div, InteractiveComponent, Render}; + use gpui::{Div, InteractiveElement, Render}; pub struct PanelStory; - impl Render for PanelStory { + impl Render for PanelStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/player_stack.rs b/crates/ui2/src/components/player_stack.rs index b883f76d35..1af3ab5c5a 100644 --- a/crates/ui2/src/components/player_stack.rs +++ b/crates/ui2/src/components/player_stack.rs @@ -1,19 +1,17 @@ +use gpui::{Div, RenderOnce}; + use crate::prelude::*; use crate::{Avatar, Facepile, PlayerWithCallStatus}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct PlayerStack { player_with_call_status: PlayerWithCallStatus, } -impl PlayerStack { - pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self { - Self { - player_with_call_status, - } - } +impl Component for PlayerStack { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { let player = self.player_with_call_status.get_player(); let followers = self @@ -59,3 +57,11 @@ impl PlayerStack { ) } } + +impl PlayerStack { + pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self { + Self { + player_with_call_status, + } + } +} diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index 7238dbc337..2d7dadac1a 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -1,8 +1,8 @@ use crate::prelude::*; use crate::{Icon, IconElement, Label, TextColor}; -use gpui::{prelude::*, red, Div, ElementId, Render, View}; +use gpui::{prelude::*, red, Div, ElementId, Render, RenderOnce, Stateful, View}; -#[derive(Component, Clone)] +#[derive(RenderOnce, Clone)] pub struct Tab { id: ElementId, title: String, @@ -20,7 +20,7 @@ struct TabDragState { title: String, } -impl Render for TabDragState { +impl Render for TabDragState { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -28,65 +28,10 @@ impl Render for TabDragState { } } -impl Tab { - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - title: "untitled".to_string(), - icon: None, - current: false, - dirty: false, - fs_status: FileSystemStatus::None, - git_status: GitStatus::None, - diagnostic_status: DiagnosticStatus::None, - close_side: IconSide::Right, - } - } +impl Component for Tab { + type Rendered = Stateful>; - pub fn current(mut self, current: bool) -> Self { - self.current = current; - self - } - - pub fn title(mut self, title: String) -> Self { - self.title = title; - self - } - - pub fn icon(mut self, icon: I) -> Self - where - I: Into>, - { - self.icon = icon.into(); - self - } - - pub fn dirty(mut self, dirty: bool) -> Self { - self.dirty = dirty; - self - } - - pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self { - self.fs_status = fs_status; - self - } - - pub fn git_status(mut self, git_status: GitStatus) -> Self { - self.git_status = git_status; - self - } - - pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self { - self.diagnostic_status = diagnostic_status; - self - } - - pub fn close_side(mut self, close_side: IconSide) -> Self { - self.close_side = close_side; - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict; let is_deleted = self.fs_status == FileSystemStatus::Deleted; @@ -164,6 +109,65 @@ impl Tab { } } +impl Tab { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + title: "untitled".to_string(), + icon: None, + current: false, + dirty: false, + fs_status: FileSystemStatus::None, + git_status: GitStatus::None, + diagnostic_status: DiagnosticStatus::None, + close_side: IconSide::Right, + } + } + + pub fn current(mut self, current: bool) -> Self { + self.current = current; + self + } + + pub fn title(mut self, title: String) -> Self { + self.title = title; + self + } + + pub fn icon(mut self, icon: I) -> Self + where + I: Into>, + { + self.icon = icon.into(); + self + } + + pub fn dirty(mut self, dirty: bool) -> Self { + self.dirty = dirty; + self + } + + pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self { + self.fs_status = fs_status; + self + } + + pub fn git_status(mut self, git_status: GitStatus) -> Self { + self.git_status = git_status; + self + } + + pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self { + self.diagnostic_status = diagnostic_status; + self + } + + pub fn close_side(mut self, close_side: IconSide) -> Self { + self.close_side = close_side; + self + } +} + #[cfg(feature = "stories")] pub use stories::*; @@ -175,7 +179,7 @@ mod stories { pub struct TabStory; - impl Render for TabStory { + impl Render for TabStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/toast.rs b/crates/ui2/src/components/toast.rs index a40f3cace4..153f9c7fdb 100644 --- a/crates/ui2/src/components/toast.rs +++ b/crates/ui2/src/components/toast.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -use gpui::Element; -use gpui::{prelude::*, AnyElement}; +use gpui::{prelude::*, AnyElement, RenderOnce}; +use gpui::{Div, Element}; use smallvec::SmallVec; #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] @@ -22,41 +22,37 @@ pub enum ToastOrigin { /// they are actively showing the a process in progress. /// /// Only one toast may be visible at a time. -#[derive(Element)] +#[derive(RenderOnce)] pub struct Toast { origin: ToastOrigin, children: SmallVec<[AnyElement; 2]>, } -// impl Element for Toast { -// type State = Option>; +impl Component for Toast { + type Rendered = Div; -// fn element_id(&self) -> Option { -// None -// } + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let mut div = div(); -// fn layout( -// &mut self, -// view_state: &mut V, -// _element_state: Option, -// cx: &mut ViewContext, -// ) -> (gpui::LayoutId, Self::State) { -// let mut element = self.render(view_state, cx).into_any(); -// let layout_id = element.layout(view_state, cx); -// (layout_id, Some(element)) -// } + if self.origin == ToastOrigin::Bottom { + div = div.right_1_2(); + } else { + div = div.right_2(); + } -// fn paint( -// self, -// bounds: gpui::Bounds, -// view_state: &mut V, -// element: &mut Self::State, -// cx: &mut ViewContext, -// ) { -// let element = element.take().unwrap(); -// element.paint(view_state, cx); -// } -// } + div.z_index(5) + .absolute() + .bottom_9() + .flex() + .py_1() + .px_1p5() + .rounded_lg() + .shadow_md() + .overflow_hidden() + .bg(cx.theme().colors().elevated_surface_background) + .children(self.children) + } +} impl Toast { pub fn new(origin: ToastOrigin) -> Self { @@ -89,7 +85,7 @@ impl Toast { } } -impl ParentComponent for Toast { +impl ParentElement for Toast { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } @@ -108,7 +104,7 @@ mod stories { pub struct ToastStory; - impl Render for ToastStory { + impl Render for ToastStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/components/toggle.rs b/crates/ui2/src/components/toggle.rs index 5919a19fc6..afd03c8b89 100644 --- a/crates/ui2/src/components/toggle.rs +++ b/crates/ui2/src/components/toggle.rs @@ -1,4 +1,4 @@ -use gpui::{div, Component, Element, ParentComponent}; +use gpui::{div, Element, ParentElement}; use crate::{Icon, IconElement, IconSize, TextColor}; diff --git a/crates/ui2/src/components/tool_divider.rs b/crates/ui2/src/components/tool_divider.rs index ca443ccb7f..4b59ffe2d8 100644 --- a/crates/ui2/src/components/tool_divider.rs +++ b/crates/ui2/src/components/tool_divider.rs @@ -1,8 +1,17 @@ use crate::prelude::*; +use gpui::{Div, RenderOnce}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct ToolDivider; +impl Component for ToolDivider { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div().w_px().h_3().bg(cx.theme().colors().border) + } +} + impl ToolDivider { pub fn new() -> Self { Self diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index a8dae6c97f..33fadf122f 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,4 +1,4 @@ -use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext}; +use gpui::{overlay, Action, AnyView, Overlay, Render, RenderOnce, VisualContext}; use settings2::Settings; use theme2::{ActiveTheme, ThemeSettings}; @@ -67,7 +67,7 @@ impl Tooltip { } } -impl Render for Tooltip { +impl Render for Tooltip { type Element = Overlay; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index d4abb78c21..876fec555e 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -1,8 +1,8 @@ use gpui::rems; use gpui::Rems; pub use gpui::{ - div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString, - Styled, ViewContext, WindowContext, + div, Component, Element, ElementId, InteractiveElement, ParentElement, SharedString, Styled, + ViewContext, WindowContext, }; pub use crate::elevation::*; diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index bb81d6230f..37649ff8c5 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -745,11 +745,11 @@ pub fn hello_world_rust_editor_example(cx: &mut ViewContext) -> Edit PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(), vec![Symbol(vec![ HighlightedText { - text: "fn ".to_string(), + text: "fn ".into(), color: cx.theme().syntax_color("keyword"), }, HighlightedText { - text: "main".to_string(), + text: "main".into(), color: cx.theme().syntax_color("function"), }, ])], @@ -779,15 +779,15 @@ pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![ HighlightedText { - text: "fn ".to_string(), + text: "fn ".into(), color: cx.theme().syntax_color("keyword"), }, HighlightedText { - text: "main".to_string(), + text: "main".into(), color: cx.theme().syntax_color("function"), }, HighlightedText { - text: "() {".to_string(), + text: "() {".into(), color: cx.theme().colors().text, }, ], @@ -803,7 +803,7 @@ pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { text: " // Statements here are executed when the compiled binary is called." - .to_string(), + .into(), color: cx.theme().syntax_color("comment"), }], }), @@ -826,7 +826,7 @@ pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec { current: false, line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { - text: " // Print text to the console.".to_string(), + text: " // Print text to the console.".into(), color: cx.theme().syntax_color("comment"), }], }), @@ -841,15 +841,15 @@ pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![ HighlightedText { - text: " println!(".to_string(), + text: " println!(".into(), color: cx.theme().colors().text, }, HighlightedText { - text: "\"Hello, world!\"".to_string(), + text: "\"Hello, world!\"".into(), color: cx.theme().syntax_color("string"), }, HighlightedText { - text: ");".to_string(), + text: ");".into(), color: cx.theme().colors().text, }, ], @@ -864,7 +864,7 @@ pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec { current: false, line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { - text: "}".to_string(), + text: "}".into(), color: cx.theme().colors().text, }], }), @@ -882,11 +882,11 @@ pub fn hello_world_rust_editor_with_status_example(cx: &mut ViewContext Vec Vec Vec Vec Vec Vec Vec Vec Buffer { Buffer::new("terminal") - .set_title("zed — fish".to_string()) + .set_title(Some("zed — fish".into())) .set_rows(Some(BufferRows { show_line_numbers: false, rows: terminal_buffer_rows(cx), @@ -1060,31 +1060,31 @@ pub fn terminal_buffer_rows(cx: &AppContext) -> Vec { line: Some(HighlightedLine { highlighted_texts: vec![ HighlightedText { - text: "maxdeviant ".to_string(), + text: "maxdeviant ".into(), color: cx.theme().syntax_color("keyword"), }, HighlightedText { - text: "in ".to_string(), + text: "in ".into(), color: cx.theme().colors().text, }, HighlightedText { - text: "profaned-capital ".to_string(), + text: "profaned-capital ".into(), color: cx.theme().syntax_color("function"), }, HighlightedText { - text: "in ".to_string(), + text: "in ".into(), color: cx.theme().colors().text, }, HighlightedText { - text: "~/p/zed ".to_string(), + text: "~/p/zed ".into(), color: cx.theme().syntax_color("function"), }, HighlightedText { - text: "on ".to_string(), + text: "on ".into(), color: cx.theme().colors().text, }, HighlightedText { - text: " gpui2-ui ".to_string(), + text: " gpui2-ui ".into(), color: cx.theme().syntax_color("keyword"), }, ], @@ -1099,7 +1099,7 @@ pub fn terminal_buffer_rows(cx: &AppContext) -> Vec { current: false, line: Some(HighlightedLine { highlighted_texts: vec![HighlightedText { - text: "λ ".to_string(), + text: "λ ".into(), color: cx.theme().syntax_color("string"), }], }), diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index b5fef606b7..8a482e155a 100644 --- a/crates/ui2/src/story.rs +++ b/crates/ui2/src/story.rs @@ -15,23 +15,29 @@ impl Story { .bg(cx.theme().colors().background) } - pub fn title(cx: &mut ViewContext, title: &str) -> impl Element { + pub fn title( + cx: &mut ViewContext, + title: impl Into, + ) -> impl Element { div() .text_xl() .text_color(cx.theme().colors().text) - .child(title.to_owned()) + .child(title.into()) } pub fn title_for(cx: &mut ViewContext) -> impl Element { Self::title(cx, std::any::type_name::()) } - pub fn label(cx: &mut ViewContext, label: &str) -> impl Element { + pub fn label( + cx: &mut ViewContext, + label: impl Into, + ) -> impl Element { div() .mt_4() .mb_2() .text_xs() .text_color(cx.theme().colors().text) - .child(label.to_owned()) + .child(label.into()) } } diff --git a/crates/ui2/src/to_extract/assistant_panel.rs b/crates/ui2/src/to_extract/assistant_panel.rs index 708d271b5b..33f4e4b208 100644 --- a/crates/ui2/src/to_extract/assistant_panel.rs +++ b/crates/ui2/src/to_extract/assistant_panel.rs @@ -1,27 +1,17 @@ use crate::prelude::*; use crate::{Icon, IconButton, Label, Panel, PanelSide}; -use gpui::{prelude::*, rems, AbsoluteLength}; +use gpui::{prelude::*, rems, AbsoluteLength, RenderOnce}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct AssistantPanel { id: ElementId, current_side: PanelSide, } -impl AssistantPanel { - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - current_side: PanelSide::default(), - } - } +impl Component for AssistantPanel { + type Rendered = Panel; - pub fn side(mut self, side: PanelSide) -> Self { - self.current_side = side; - self - } - - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { Panel::new(self.id.clone(), cx) .children(vec![div() .flex() @@ -64,12 +54,26 @@ impl AssistantPanel { .overflow_y_scroll() .child(Label::new("Is this thing on?")), ) - .render()]) + .into_any()]) .side(self.current_side) .width(AbsoluteLength::Rems(rems(32.))) } } +impl AssistantPanel { + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + current_side: PanelSide::default(), + } + } + + pub fn side(mut self, side: PanelSide) -> Self { + self.current_side = side; + self + } +} + #[cfg(feature = "stories")] pub use stories::*; @@ -80,7 +84,7 @@ mod stories { use gpui::{Div, Render}; pub struct AssistantPanelStory; - impl Render for AssistantPanelStory { + impl Render for AssistantPanelStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/breadcrumb.rs b/crates/ui2/src/to_extract/breadcrumb.rs index 6990890054..532776fd7f 100644 --- a/crates/ui2/src/to_extract/breadcrumb.rs +++ b/crates/ui2/src/to_extract/breadcrumb.rs @@ -1,30 +1,21 @@ use crate::{h_stack, prelude::*, HighlightedText}; -use gpui::{prelude::*, Div}; +use gpui::{prelude::*, Div, Stateful}; use std::path::PathBuf; #[derive(Clone)] pub struct Symbol(pub Vec); -#[derive(Component)] +#[derive(RenderOnce)] pub struct Breadcrumb { path: PathBuf, symbols: Vec, } -impl Breadcrumb { - pub fn new(path: PathBuf, symbols: Vec) -> Self { - Self { path, symbols } - } +impl Component for Breadcrumb { + type Rendered = Stateful>; - fn render_separator(&self, cx: &WindowContext) -> Div { - div() - .child(" › ") - .text_color(cx.theme().colors().text_muted) - } - - fn render(self, view_state: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view_state: &mut V, cx: &mut ViewContext) -> Self::Rendered { let symbols_len = self.symbols.len(); - h_stack() .id("breadcrumb") .px_1() @@ -33,7 +24,9 @@ impl Breadcrumb { .rounded_md() .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .child(self.path.clone().to_str().unwrap().to_string()) + .child(SharedString::from( + self.path.clone().to_str().unwrap().to_string(), + )) .child(if !self.symbols.is_empty() { self.render_separator(cx) } else { @@ -64,6 +57,18 @@ impl Breadcrumb { } } +impl Breadcrumb { + pub fn new(path: PathBuf, symbols: Vec) -> Self { + Self { path, symbols } + } + + fn render_separator(&self, cx: &WindowContext) -> Div { + div() + .child(" › ") + .text_color(cx.theme().colors().text_muted) + } +} + #[cfg(feature = "stories")] pub use stories::*; @@ -76,7 +81,7 @@ mod stories { pub struct BreadcrumbStory; - impl Render for BreadcrumbStory { + impl Render for BreadcrumbStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -88,21 +93,21 @@ mod stories { vec![ Symbol(vec![ HighlightedText { - text: "impl ".to_string(), + text: "impl ".into(), color: cx.theme().syntax_color("keyword"), }, HighlightedText { - text: "BreadcrumbStory".to_string(), + text: "BreadcrumbStory".into(), color: cx.theme().syntax_color("function"), }, ]), Symbol(vec![ HighlightedText { - text: "fn ".to_string(), + text: "fn ".into(), color: cx.theme().syntax_color("keyword"), }, HighlightedText { - text: "render".to_string(), + text: "render".into(), color: cx.theme().syntax_color("function"), }, ]), diff --git a/crates/ui2/src/to_extract/buffer.rs b/crates/ui2/src/to_extract/buffer.rs index c8223daf2f..ac4665614b 100644 --- a/crates/ui2/src/to_extract/buffer.rs +++ b/crates/ui2/src/to_extract/buffer.rs @@ -1,4 +1,4 @@ -use gpui::{Hsla, WindowContext}; +use gpui::{Div, Hsla, RenderOnce, WindowContext}; use crate::prelude::*; use crate::{h_stack, v_stack, Icon, IconElement}; @@ -11,7 +11,7 @@ pub struct PlayerCursor { #[derive(Default, PartialEq, Clone)] pub struct HighlightedText { - pub text: String, + pub text: SharedString, pub color: Hsla, } @@ -107,7 +107,7 @@ impl BufferRow { } } -#[derive(Component, Clone)] +#[derive(RenderOnce, Clone)] pub struct Buffer { id: ElementId, rows: Option, @@ -117,6 +117,21 @@ pub struct Buffer { path: Option, } +impl Component for Buffer { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let rows = self.render_rows(cx); + + v_stack() + .flex_1() + .w_full() + .h_full() + .bg(cx.theme().colors().editor_background) + .children(rows) + } +} + impl Buffer { pub fn new(id: impl Into) -> Self { Self { @@ -186,7 +201,7 @@ impl Buffer { h_stack().justify_end().px_0p5().w_3().child( div() .text_color(line_number_color) - .child(row.line_number.to_string()), + .child(SharedString::from(row.line_number.to_string())), ), ) }) @@ -239,7 +254,7 @@ mod stories { pub struct BufferStory; - impl Render for BufferStory { + impl Render for BufferStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/buffer_search.rs b/crates/ui2/src/to_extract/buffer_search.rs index 996ac6d253..3a89465911 100644 --- a/crates/ui2/src/to_extract/buffer_search.rs +++ b/crates/ui2/src/to_extract/buffer_search.rs @@ -1,7 +1,6 @@ -use gpui::{Div, Render, View, VisualContext}; - use crate::prelude::*; use crate::{h_stack, Icon, IconButton, Input, TextColor}; +use gpui::{Div, Render, RenderOnce, View, VisualContext}; #[derive(Clone)] pub struct BufferSearch { @@ -26,7 +25,7 @@ impl BufferSearch { } } -impl Render for BufferSearch { +impl Render for BufferSearch { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { diff --git a/crates/ui2/src/to_extract/chat_panel.rs b/crates/ui2/src/to_extract/chat_panel.rs index 528e40f903..00655364bf 100644 --- a/crates/ui2/src/to_extract/chat_panel.rs +++ b/crates/ui2/src/to_extract/chat_panel.rs @@ -1,27 +1,17 @@ use crate::{prelude::*, Icon, IconButton, Input, Label}; use chrono::NaiveDateTime; -use gpui::prelude::*; +use gpui::{prelude::*, Div, Stateful}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct ChatPanel { element_id: ElementId, messages: Vec, } -impl ChatPanel { - pub fn new(element_id: impl Into) -> Self { - Self { - element_id: element_id.into(), - messages: Vec::new(), - } - } +impl Component for ChatPanel { + type Rendered = Stateful>; - pub fn messages(mut self, messages: Vec) -> Self { - self.messages = messages; - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div() .id(self.element_id.clone()) .flex() @@ -67,23 +57,31 @@ impl ChatPanel { } } -#[derive(Component)] +impl ChatPanel { + pub fn new(element_id: impl Into) -> Self { + Self { + element_id: element_id.into(), + messages: Vec::new(), + } + } + + pub fn messages(mut self, messages: Vec) -> Self { + self.messages = messages; + self + } +} + +#[derive(RenderOnce)] pub struct ChatMessage { author: String, text: String, sent_at: NaiveDateTime, } -impl ChatMessage { - pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self { - Self { - author, - text, - sent_at, - } - } +impl Component for ChatMessage { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div() .flex() .flex_col() @@ -101,6 +99,16 @@ impl ChatMessage { } } +impl ChatMessage { + pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self { + Self { + author, + text, + sent_at, + } + } +} + #[cfg(feature = "stories")] pub use stories::*; @@ -115,7 +123,7 @@ mod stories { pub struct ChatPanelStory; - impl Render for ChatPanelStory { + impl Render for ChatPanelStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/collab_panel.rs b/crates/ui2/src/to_extract/collab_panel.rs index 55efbb5ec5..e5fafa62f1 100644 --- a/crates/ui2/src/to_extract/collab_panel.rs +++ b/crates/ui2/src/to_extract/collab_panel.rs @@ -2,19 +2,17 @@ use crate::{ prelude::*, static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader, Toggle, }; -use gpui::prelude::*; +use gpui::{prelude::*, Div, Stateful}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct CollabPanel { id: ElementId, } -impl CollabPanel { - pub fn new(id: impl Into) -> Self { - Self { id: id.into() } - } +impl Component for CollabPanel { + type Rendered = Stateful>; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { v_stack() .id(self.id.clone()) .h_full() @@ -86,6 +84,12 @@ impl CollabPanel { } } +impl CollabPanel { + pub fn new(id: impl Into) -> Self { + Self { id: id.into() } + } +} + #[cfg(feature = "stories")] pub use stories::*; @@ -97,7 +101,7 @@ mod stories { pub struct CollabPanelStory; - impl Render for CollabPanelStory { + impl Render for CollabPanelStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/command_palette.rs b/crates/ui2/src/to_extract/command_palette.rs index 353e1daac8..032e3bc639 100644 --- a/crates/ui2/src/to_extract/command_palette.rs +++ b/crates/ui2/src/to_extract/command_palette.rs @@ -1,17 +1,15 @@ use crate::prelude::*; use crate::{example_editor_actions, OrderMethod, Palette}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct CommandPalette { id: ElementId, } -impl CommandPalette { - pub fn new(id: impl Into) -> Self { - Self { id: id.into() } - } +impl Component for CommandPalette { + type Rendered = Stateful>; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div().id(self.id.clone()).child( Palette::new("palette") .items(example_editor_actions()) @@ -22,6 +20,13 @@ impl CommandPalette { } } +impl CommandPalette { + pub fn new(id: impl Into) -> Self { + Self { id: id.into() } + } +} + +use gpui::{Div, RenderOnce, Stateful}; #[cfg(feature = "stories")] pub use stories::*; @@ -35,7 +40,7 @@ mod stories { pub struct CommandPaletteStory; - impl Render for CommandPaletteStory { + impl Render for CommandPaletteStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/copilot.rs b/crates/ui2/src/to_extract/copilot.rs index b07c562c13..d0b11a3330 100644 --- a/crates/ui2/src/to_extract/copilot.rs +++ b/crates/ui2/src/to_extract/copilot.rs @@ -1,39 +1,42 @@ use crate::{prelude::*, Button, Label, Modal, TextColor}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct CopilotModal { id: ElementId, } +impl Component for CopilotModal { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div().id(self.id.clone()).child( + Modal::new("some-id") + .title("Connect Copilot to Zed") + .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted)) + .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)), + ) + } +} + impl CopilotModal { pub fn new(id: impl Into) -> Self { Self { id: id.into() } } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - div().id(self.id.clone()).child( - Modal::new("some-id") - .title("Connect Copilot to Zed") - .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted)) - .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)), - ) - } } +use gpui::{Div, RenderOnce, Stateful}; #[cfg(feature = "stories")] pub use stories::*; #[cfg(feature = "stories")] mod stories { - use gpui::{Div, Render}; - - use crate::Story; - use super::*; + use crate::Story; + use gpui::{Div, Render}; pub struct CopilotModalStory; - impl Render for CopilotModalStory { + impl Render for CopilotModalStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/editor_pane.rs b/crates/ui2/src/to_extract/editor_pane.rs index bd34c22805..799716c270 100644 --- a/crates/ui2/src/to_extract/editor_pane.rs +++ b/crates/ui2/src/to_extract/editor_pane.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use gpui::{Div, Render, View, VisualContext}; +use gpui::{Div, Render, RenderOnce, View, VisualContext}; use crate::prelude::*; use crate::{ @@ -47,7 +47,7 @@ impl EditorPane { } } -impl Render for EditorPane { +impl Render for EditorPane { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { diff --git a/crates/ui2/src/to_extract/language_selector.rs b/crates/ui2/src/to_extract/language_selector.rs index e05145e8c8..c6a8457e0b 100644 --- a/crates/ui2/src/to_extract/language_selector.rs +++ b/crates/ui2/src/to_extract/language_selector.rs @@ -1,11 +1,36 @@ use crate::prelude::*; use crate::{OrderMethod, Palette, PaletteItem}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct LanguageSelector { id: ElementId, } +impl Component for LanguageSelector { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div().id(self.id.clone()).child( + Palette::new("palette") + .items(vec![ + PaletteItem::new("C"), + PaletteItem::new("C++"), + PaletteItem::new("CSS"), + PaletteItem::new("Elixir"), + PaletteItem::new("Elm"), + PaletteItem::new("ERB"), + PaletteItem::new("Rust (current)"), + PaletteItem::new("Scheme"), + PaletteItem::new("TOML"), + PaletteItem::new("TypeScript"), + ]) + .placeholder("Select a language...") + .empty_string("No matches") + .default_order(OrderMethod::Ascending), + ) + } +} + impl LanguageSelector { pub fn new(id: impl Into) -> Self { Self { id: id.into() } @@ -33,6 +58,7 @@ impl LanguageSelector { } } +use gpui::{Div, RenderOnce, Stateful}; #[cfg(feature = "stories")] pub use stories::*; @@ -44,7 +70,7 @@ mod stories { pub struct LanguageSelectorStory; - impl Render for LanguageSelectorStory { + impl Render for LanguageSelectorStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/multi_buffer.rs b/crates/ui2/src/to_extract/multi_buffer.rs index 186303365b..7967a1e590 100644 --- a/crates/ui2/src/to_extract/multi_buffer.rs +++ b/crates/ui2/src/to_extract/multi_buffer.rs @@ -1,17 +1,15 @@ use crate::prelude::*; use crate::{v_stack, Buffer, Icon, IconButton, Label}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct MultiBuffer { buffers: Vec, } -impl MultiBuffer { - pub fn new(buffers: Vec) -> Self { - Self { buffers } - } +impl Component for MultiBuffer { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { v_stack() .w_full() .h_full() @@ -33,6 +31,13 @@ impl MultiBuffer { } } +impl MultiBuffer { + pub fn new(buffers: Vec) -> Self { + Self { buffers } + } +} + +use gpui::{Div, RenderOnce}; #[cfg(feature = "stories")] pub use stories::*; @@ -44,7 +49,7 @@ mod stories { pub struct MultiBufferStory; - impl Render for MultiBufferStory { + impl Render for MultiBufferStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/notifications_panel.rs b/crates/ui2/src/to_extract/notifications_panel.rs index fa7f8c2bd5..f45b12f0c2 100644 --- a/crates/ui2/src/to_extract/notifications_panel.rs +++ b/crates/ui2/src/to_extract/notifications_panel.rs @@ -3,19 +3,17 @@ use crate::{ v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LineHeightStyle, ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, TextColor, UnreadIndicator, }; -use gpui::prelude::*; +use gpui::{prelude::*, Div, Stateful}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct NotificationsPanel { id: ElementId, } -impl NotificationsPanel { - pub fn new(id: impl Into) -> Self { - Self { id: id.into() } - } +impl Component for NotificationsPanel { + type Rendered = Stateful>; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div() .id(self.id.clone()) .flex() @@ -56,6 +54,12 @@ impl NotificationsPanel { } } +impl NotificationsPanel { + pub fn new(id: impl Into) -> Self { + Self { id: id.into() } + } +} + pub struct NotificationAction { button: ButtonOrIconButton, tooltip: SharedString, @@ -102,7 +106,7 @@ impl Default for NotificationHandlers { } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct Notification { id: ElementId, slot: ActorOrIcon, @@ -116,6 +120,85 @@ pub struct Notification { handlers: NotificationHandlers, } +impl Component for Notification { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div() + .relative() + .id(self.id.clone()) + .p_1() + .flex() + .flex_col() + .w_full() + .children( + Some( + div() + .absolute() + .left(px(3.0)) + .top_3() + .z_index(2) + .child(UnreadIndicator::new()), + ) + .filter(|_| self.unread), + ) + .child( + v_stack() + .z_index(1) + .gap_1() + .w_full() + .child( + h_stack() + .w_full() + .gap_2() + .child(self.render_slot(cx)) + .child(div().flex_1().child(Label::new(self.message.clone()))), + ) + .child( + h_stack() + .justify_between() + .child( + h_stack() + .gap_1() + .child( + Label::new(naive_format_distance_from_now( + self.date_received, + true, + true, + )) + .color(TextColor::Muted), + ) + .child(self.render_meta_items(cx)), + ) + .child(match (self.actions, self.action_taken) { + // Show nothing + (None, _) => div(), + // Show the taken_message + (Some(_), Some(action_taken)) => h_stack() + .children(action_taken.taken_message.0.map(|icon| { + IconElement::new(icon).color(crate::TextColor::Muted) + })) + .child( + Label::new(action_taken.taken_message.1.clone()) + .color(TextColor::Muted), + ), + // Show the actions + (Some(actions), None) => { + h_stack().children(actions.map(|action| match action.button { + ButtonOrIconButton::Button(button) => { + button.render_into_any() + } + ButtonOrIconButton::IconButton(icon_button) => { + icon_button.render_into_any() + } + })) + } + }), + ), + ) + } +} + impl Notification { fn new( id: ElementId, @@ -262,85 +345,10 @@ impl Notification { fn render_slot(&self, cx: &mut ViewContext) -> impl Element { match &self.slot { - ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(), - ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(), + ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render_into_any(), + ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render_into_any(), } } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - div() - .relative() - .id(self.id.clone()) - .p_1() - .flex() - .flex_col() - .w_full() - .children( - Some( - div() - .absolute() - .left(px(3.0)) - .top_3() - .z_index(2) - .child(UnreadIndicator::new()), - ) - .filter(|_| self.unread), - ) - .child( - v_stack() - .z_index(1) - .gap_1() - .w_full() - .child( - h_stack() - .w_full() - .gap_2() - .child(self.render_slot(cx)) - .child(div().flex_1().child(Label::new(self.message.clone()))), - ) - .child( - h_stack() - .justify_between() - .child( - h_stack() - .gap_1() - .child( - Label::new(naive_format_distance_from_now( - self.date_received, - true, - true, - )) - .color(TextColor::Muted), - ) - .child(self.render_meta_items(cx)), - ) - .child(match (self.actions, self.action_taken) { - // Show nothing - (None, _) => div(), - // Show the taken_message - (Some(_), Some(action_taken)) => h_stack() - .children(action_taken.taken_message.0.map(|icon| { - IconElement::new(icon).color(crate::TextColor::Muted) - })) - .child( - Label::new(action_taken.taken_message.1.clone()) - .color(TextColor::Muted), - ), - // Show the actions - (Some(actions), None) => { - h_stack().children(actions.map(|action| match action.button { - ButtonOrIconButton::Button(button) => { - Component::render(button) - } - ButtonOrIconButton::IconButton(icon_button) => { - Component::render(icon_button) - } - })) - } - }), - ), - ) - } } use chrono::NaiveDateTime; @@ -356,7 +364,7 @@ mod stories { pub struct NotificationsPanelStory; - impl Render for NotificationsPanelStory { + impl Render for NotificationsPanelStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/panes.rs b/crates/ui2/src/to_extract/panes.rs index 3b75d55d77..92bf87783f 100644 --- a/crates/ui2/src/to_extract/panes.rs +++ b/crates/ui2/src/to_extract/panes.rs @@ -1,4 +1,7 @@ -use gpui::{hsla, red, AnyElement, ElementId, ExternalPaths, Hsla, Length, Size, View}; +use gpui::{ + hsla, red, AnyElement, Div, ElementId, ExternalPaths, Hsla, Length, RenderOnce, Size, Stateful, + View, +}; use smallvec::SmallVec; use crate::prelude::*; @@ -10,7 +13,7 @@ pub enum SplitDirection { Vertical, } -#[derive(Component)] +#[derive(RenderOnce)] pub struct Pane { id: ElementId, size: Size, @@ -18,24 +21,10 @@ pub struct Pane { children: SmallVec<[AnyElement; 2]>, } -impl Pane { - pub fn new(id: impl Into, size: Size) -> Self { - // Fill is only here for debugging purposes, remove before release +impl Component for Pane { + type Rendered = Stateful>; - Self { - id: id.into(), - size, - fill: hsla(0.3, 0.3, 0.3, 1.), - children: SmallVec::new(), - } - } - - pub fn fill(mut self, fill: Hsla) -> Self { - self.fill = fill; - self - } - - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div() .id(self.id.clone()) .flex() @@ -59,37 +48,41 @@ impl Pane { } } -impl ParentComponent for Pane { +impl Pane { + pub fn new(id: impl Into, size: Size) -> Self { + // Fill is only here for debugging purposes, remove before release + + Self { + id: id.into(), + size, + fill: hsla(0.3, 0.3, 0.3, 1.), + children: SmallVec::new(), + } + } + + pub fn fill(mut self, fill: Hsla) -> Self { + self.fill = fill; + self + } +} + +impl ParentElement for Pane { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } } -#[derive(Component)] +#[derive(RenderOnce)] pub struct PaneGroup { groups: Vec>, panes: Vec>, split_direction: SplitDirection, } -impl PaneGroup { - pub fn new_groups(groups: Vec>, split_direction: SplitDirection) -> Self { - Self { - groups, - panes: Vec::new(), - split_direction, - } - } +impl Component for PaneGroup { + type Rendered = Div; - pub fn new_panes(panes: Vec>, split_direction: SplitDirection) -> Self { - Self { - groups: Vec::new(), - panes, - split_direction, - } - } - - fn render(self, view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { if !self.panes.is_empty() { let el = div() .flex() @@ -126,3 +119,21 @@ impl PaneGroup { unreachable!() } } + +impl PaneGroup { + pub fn new_groups(groups: Vec>, split_direction: SplitDirection) -> Self { + Self { + groups, + panes: Vec::new(), + split_direction, + } + } + + pub fn new_panes(panes: Vec>, split_direction: SplitDirection) -> Self { + Self { + groups: Vec::new(), + panes, + split_direction, + } + } +} diff --git a/crates/ui2/src/to_extract/project_panel.rs b/crates/ui2/src/to_extract/project_panel.rs index ccaaecf2ce..06735356c7 100644 --- a/crates/ui2/src/to_extract/project_panel.rs +++ b/crates/ui2/src/to_extract/project_panel.rs @@ -3,12 +3,50 @@ use crate::{ ListHeader, }; use gpui::prelude::*; +use gpui::Div; +use gpui::Stateful; -#[derive(Component)] +#[derive(RenderOnce)] pub struct ProjectPanel { id: ElementId, } +impl Component for ProjectPanel { + type Rendered = Stateful>; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div() + .id(self.id.clone()) + .flex() + .flex_col() + .size_full() + .bg(cx.theme().colors().surface_background) + .child( + div() + .id("project-panel-contents") + .w_full() + .flex() + .flex_col() + .overflow_y_scroll() + .child( + List::new(static_project_panel_single_items()) + .header(ListHeader::new("FILES")) + .empty_message("No files in directory"), + ) + .child( + List::new(static_project_panel_project_items()) + .header(ListHeader::new("PROJECT")) + .empty_message("No folders in directory"), + ), + ) + .child( + Input::new("Find something...") + .value("buffe".to_string()) + .state(InteractionState::Focused), + ) + } +} + impl ProjectPanel { pub fn new(id: impl Into) -> Self { Self { id: id.into() } @@ -59,7 +97,7 @@ mod stories { pub struct ProjectPanelStory; - impl Render for ProjectPanelStory { + impl Render for ProjectPanelStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/recent_projects.rs b/crates/ui2/src/to_extract/recent_projects.rs index 2db7d94eac..3ebffb6bef 100644 --- a/crates/ui2/src/to_extract/recent_projects.rs +++ b/crates/ui2/src/to_extract/recent_projects.rs @@ -1,17 +1,15 @@ use crate::prelude::*; use crate::{OrderMethod, Palette, PaletteItem}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct RecentProjects { id: ElementId, } -impl RecentProjects { - pub fn new(id: impl Into) -> Self { - Self { id: id.into() } - } +impl Component for RecentProjects { + type Rendered = Stateful>; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div().id(self.id.clone()).child( Palette::new("palette") .items(vec![ @@ -29,6 +27,13 @@ impl RecentProjects { } } +impl RecentProjects { + pub fn new(id: impl Into) -> Self { + Self { id: id.into() } + } +} + +use gpui::{Div, RenderOnce, Stateful}; #[cfg(feature = "stories")] pub use stories::*; @@ -40,7 +45,7 @@ mod stories { pub struct RecentProjectsStory; - impl Render for RecentProjectsStory { + impl Render for RecentProjectsStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/status_bar.rs b/crates/ui2/src/to_extract/status_bar.rs index eee96ecad8..b07c45aa75 100644 --- a/crates/ui2/src/to_extract/status_bar.rs +++ b/crates/ui2/src/to_extract/status_bar.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use gpui::{Div, RenderOnce}; + use crate::prelude::*; use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace}; @@ -28,14 +30,31 @@ impl Default for ToolGroup { } } -#[derive(Component)] -#[component(view_type = "Workspace")] +#[derive(RenderOnce)] +#[view = "Workspace"] pub struct StatusBar { left_tools: Option, right_tools: Option, bottom_tools: Option, } +impl Component for StatusBar { + type Rendered = Div; + + fn render(self, view: &mut Workspace, cx: &mut ViewContext) -> Self::Rendered { + div() + .py_0p5() + .px_1() + .flex() + .items_center() + .justify_between() + .w_full() + .bg(cx.theme().colors().status_bar_background) + .child(self.left_tools(view, cx)) + .child(self.right_tools(view, cx)) + } +} + impl StatusBar { pub fn new() -> Self { Self { @@ -81,28 +100,7 @@ impl StatusBar { self } - fn render( - self, - view: &mut Workspace, - cx: &mut ViewContext, - ) -> impl Component { - div() - .py_0p5() - .px_1() - .flex() - .items_center() - .justify_between() - .w_full() - .bg(cx.theme().colors().status_bar_background) - .child(self.left_tools(view, cx)) - .child(self.right_tools(view, cx)) - } - - fn left_tools( - &self, - workspace: &mut Workspace, - cx: &WindowContext, - ) -> impl Component { + fn left_tools(&self, workspace: &mut Workspace, cx: &WindowContext) -> impl Element { div() .flex() .items_center() @@ -133,7 +131,7 @@ impl StatusBar { &self, workspace: &mut Workspace, cx: &WindowContext, - ) -> impl Component { + ) -> impl Element { div() .flex() .items_center() diff --git a/crates/ui2/src/to_extract/tab_bar.rs b/crates/ui2/src/to_extract/tab_bar.rs index 8888620dae..80c165b97e 100644 --- a/crates/ui2/src/to_extract/tab_bar.rs +++ b/crates/ui2/src/to_extract/tab_bar.rs @@ -1,7 +1,9 @@ use crate::{prelude::*, Icon, IconButton, Tab}; use gpui::prelude::*; +use gpui::Div; +use gpui::Stateful; -#[derive(Component)] +#[derive(RenderOnce)] pub struct TabBar { id: ElementId, /// Backwards, Forwards @@ -9,21 +11,10 @@ pub struct TabBar { tabs: Vec, } -impl TabBar { - pub fn new(id: impl Into, tabs: Vec) -> Self { - Self { - id: id.into(), - can_navigate: (false, false), - tabs, - } - } +impl Component for TabBar { + type Rendered = Stateful>; - pub fn can_navigate(mut self, can_navigate: (bool, bool)) -> Self { - self.can_navigate = can_navigate; - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { let (can_navigate_back, can_navigate_forward) = self.can_navigate; div() @@ -92,6 +83,21 @@ impl TabBar { } } +impl TabBar { + pub fn new(id: impl Into, tabs: Vec) -> Self { + Self { + id: id.into(), + can_navigate: (false, false), + tabs, + } + } + + pub fn can_navigate(mut self, can_navigate: (bool, bool)) -> Self { + self.can_navigate = can_navigate; + self + } +} + use gpui::ElementId; #[cfg(feature = "stories")] pub use stories::*; @@ -104,7 +110,7 @@ mod stories { pub struct TabBarStory; - impl Render for TabBarStory { + impl Render for TabBarStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/terminal.rs b/crates/ui2/src/to_extract/terminal.rs index d639986082..4cc43d4983 100644 --- a/crates/ui2/src/to_extract/terminal.rs +++ b/crates/ui2/src/to_extract/terminal.rs @@ -1,11 +1,78 @@ -use gpui::{relative, rems, Size}; - use crate::prelude::*; use crate::{Icon, IconButton, Pane, Tab}; +use gpui::{relative, rems, Div, RenderOnce, Size}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct Terminal; +impl Component for Terminal { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let can_navigate_back = true; + let can_navigate_forward = false; + + div() + .flex() + .flex_col() + .w_full() + .child( + // Terminal Tabs. + div() + .w_full() + .flex() + .bg(cx.theme().colors().surface_background) + .child( + div().px_1().flex().flex_none().gap_2().child( + div() + .flex() + .items_center() + .gap_px() + .child( + IconButton::new("arrow_left", Icon::ArrowLeft).state( + InteractionState::Enabled.if_enabled(can_navigate_back), + ), + ) + .child(IconButton::new("arrow_right", Icon::ArrowRight).state( + InteractionState::Enabled.if_enabled(can_navigate_forward), + )), + ), + ) + .child( + div().w_0().flex_1().h_full().child( + div() + .flex() + .child( + Tab::new(1) + .title("zed — fish".to_string()) + .icon(Icon::Terminal) + .close_side(IconSide::Right) + .current(true), + ) + .child( + Tab::new(2) + .title("zed — fish".to_string()) + .icon(Icon::Terminal) + .close_side(IconSide::Right) + .current(false), + ), + ), + ), + ) + // Terminal Pane. + .child( + Pane::new( + "terminal", + Size { + width: relative(1.).into(), + height: rems(36.).into(), + }, + ) + .child(crate::static_data::terminal_buffer(cx)), + ) + } +} + impl Terminal { pub fn new() -> Self { Self @@ -86,7 +153,7 @@ mod stories { use gpui::{Div, Render}; pub struct TerminalStory; - impl Render for TerminalStory { + impl Render for TerminalStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/theme_selector.rs b/crates/ui2/src/to_extract/theme_selector.rs index 175d126133..7b08195a57 100644 --- a/crates/ui2/src/to_extract/theme_selector.rs +++ b/crates/ui2/src/to_extract/theme_selector.rs @@ -1,17 +1,16 @@ use crate::prelude::*; use crate::{OrderMethod, Palette, PaletteItem}; -#[derive(Component)] +#[derive(RenderOnce)] pub struct ThemeSelector { id: ElementId, } -impl ThemeSelector { - pub fn new(id: impl Into) -> Self { - Self { id: id.into() } - } +impl Component for ThemeSelector { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + let cx: &mut ViewContext = cx; div().child( Palette::new(self.id.clone()) .items(vec![ @@ -34,6 +33,13 @@ impl ThemeSelector { } } +impl ThemeSelector { + pub fn new(id: impl Into) -> Self { + Self { id: id.into() } + } +} + +use gpui::{Div, RenderOnce}; #[cfg(feature = "stories")] pub use stories::*; @@ -47,7 +53,7 @@ mod stories { pub struct ThemeSelectorStory; - impl Render for ThemeSelectorStory { + impl Render for ThemeSelectorStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/title_bar.rs b/crates/ui2/src/to_extract/title_bar.rs index 1a106cbf7a..167d8ca0bd 100644 --- a/crates/ui2/src/to_extract/title_bar.rs +++ b/crates/ui2/src/to_extract/title_bar.rs @@ -1,7 +1,7 @@ use std::sync::atomic::AtomicBool; use std::sync::Arc; -use gpui::{Div, Render, View, VisualContext}; +use gpui::{Div, Render, RenderOnce, View, VisualContext}; use crate::prelude::*; use crate::settings::user_settings; @@ -85,7 +85,7 @@ impl TitleBar { } } -impl Render for TitleBar { +impl Render for TitleBar { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { @@ -205,7 +205,7 @@ mod stories { } } - impl Render for TitleBarStory { + impl Render for TitleBarStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { diff --git a/crates/ui2/src/to_extract/toolbar.rs b/crates/ui2/src/to_extract/toolbar.rs index b7738ffb1c..884a72d2c7 100644 --- a/crates/ui2/src/to_extract/toolbar.rs +++ b/crates/ui2/src/to_extract/toolbar.rs @@ -1,4 +1,4 @@ -use gpui::AnyElement; +use gpui::{AnyElement, Div, RenderOnce}; use smallvec::SmallVec; use crate::prelude::*; @@ -6,12 +6,26 @@ use crate::prelude::*; #[derive(Clone)] pub struct ToolbarItem {} -#[derive(Component)] +#[derive(RenderOnce)] pub struct Toolbar { left_items: SmallVec<[AnyElement; 2]>, right_items: SmallVec<[AnyElement; 2]>, } +impl Component for Toolbar { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div() + .bg(cx.theme().colors().toolbar_background) + .p_2() + .flex() + .justify_between() + .child(div().flex().children(self.left_items)) + .child(div().flex().children(self.right_items)) + } +} + impl Toolbar { pub fn new() -> Self { Self { @@ -20,49 +34,39 @@ impl Toolbar { } } - pub fn left_item(mut self, child: impl Element) -> Self + pub fn left_item(mut self, child: impl RenderOnce) -> Self where Self: Sized, { - self.left_items.push(child.render()); + self.left_items.push(child.render_into_any()); self } - pub fn left_items(mut self, iter: impl IntoIterator>) -> Self + pub fn left_items(mut self, iter: impl IntoIterator>) -> Self where Self: Sized, { self.left_items - .extend(iter.into_iter().map(|item| item.render())); + .extend(iter.into_iter().map(|item| item.render_into_any())); self } - pub fn right_item(mut self, child: impl Element) -> Self + pub fn right_item(mut self, child: impl RenderOnce) -> Self where Self: Sized, { - self.right_items.push(child.render()); + self.right_items.push(child.render_into_any()); self } - pub fn right_items(mut self, iter: impl IntoIterator>) -> Self + pub fn right_items(mut self, iter: impl IntoIterator>) -> Self where Self: Sized, { self.right_items - .extend(iter.into_iter().map(|item| item.render())); + .extend(iter.into_iter().map(|item| item.render_into_any())); self } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { - div() - .bg(cx.theme().colors().toolbar_background) - .p_2() - .flex() - .justify_between() - .child(div().flex().children(self.left_items)) - .child(div().flex().children(self.right_items)) - } } #[cfg(feature = "stories")] @@ -81,7 +85,7 @@ mod stories { pub struct ToolbarStory; - impl Render for ToolbarStory { + impl Render for ToolbarStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -95,21 +99,21 @@ mod stories { vec![ Symbol(vec![ HighlightedText { - text: "impl ".to_string(), + text: "impl ".into(), color: cx.theme().syntax_color("keyword"), }, HighlightedText { - text: "ToolbarStory".to_string(), + text: "ToolbarStory".into(), color: cx.theme().syntax_color("function"), }, ]), Symbol(vec![ HighlightedText { - text: "fn ".to_string(), + text: "fn ".into(), color: cx.theme().syntax_color("keyword"), }, HighlightedText { - text: "render".to_string(), + text: "render".into(), color: cx.theme().syntax_color("function"), }, ]), diff --git a/crates/ui2/src/to_extract/traffic_lights.rs b/crates/ui2/src/to_extract/traffic_lights.rs index 8cab5594e6..5164e74159 100644 --- a/crates/ui2/src/to_extract/traffic_lights.rs +++ b/crates/ui2/src/to_extract/traffic_lights.rs @@ -7,21 +7,16 @@ enum TrafficLightColor { Green, } -#[derive(Component)] +#[derive(RenderOnce)] struct TrafficLight { color: TrafficLightColor, window_has_focus: bool, } -impl TrafficLight { - fn new(color: TrafficLightColor, window_has_focus: bool) -> Self { - Self { - color, - window_has_focus, - } - } +impl Component for TrafficLight { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { let system_colors = &cx.theme().styles.system; let fill = match (self.window_has_focus, self.color) { @@ -35,24 +30,24 @@ impl TrafficLight { } } -#[derive(Component)] +impl TrafficLight { + fn new(color: TrafficLightColor, window_has_focus: bool) -> Self { + Self { + color, + window_has_focus, + } + } +} + +#[derive(RenderOnce)] pub struct TrafficLights { window_has_focus: bool, } -impl TrafficLights { - pub fn new() -> Self { - Self { - window_has_focus: true, - } - } +impl Component for TrafficLights { + type Rendered = Div; - pub fn window_has_focus(mut self, window_has_focus: bool) -> Self { - self.window_has_focus = window_has_focus; - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div() .flex() .items_center() @@ -72,6 +67,20 @@ impl TrafficLights { } } +impl TrafficLights { + pub fn new() -> Self { + Self { + window_has_focus: true, + } + } + + pub fn window_has_focus(mut self, window_has_focus: bool) -> Self { + self.window_has_focus = window_has_focus; + self + } +} + +use gpui::{Div, RenderOnce}; #[cfg(feature = "stories")] pub use stories::*; @@ -85,7 +94,7 @@ mod stories { pub struct TrafficLightsStory; - impl Render for TrafficLightsStory { + impl Render for TrafficLightsStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/ui2/src/to_extract/workspace.rs b/crates/ui2/src/to_extract/workspace.rs index 0451a9d032..f31009cbb5 100644 --- a/crates/ui2/src/to_extract/workspace.rs +++ b/crates/ui2/src/to_extract/workspace.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use chrono::DateTime; -use gpui::{px, relative, Div, Render, Size, View, VisualContext}; +use gpui::{px, relative, Div, Render, RenderOnce, Size, View, VisualContext}; use settings2::Settings; use theme2::ThemeSettings; @@ -191,7 +191,7 @@ impl Workspace { } } -impl Render for Workspace { +impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Div { @@ -388,7 +388,7 @@ mod stories { } } - impl Render for WorkspaceStory { + impl Render for WorkspaceStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 9603875aed..406a7a33d1 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,7 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId, - EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled, + EventEmitter, FocusHandle, FocusableView, ParentElement, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 327e7c09ed..b0f905f845 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -2,8 +2,8 @@ use std::any::TypeId; use crate::{ItemHandle, Pane}; use gpui::{ - div, AnyView, Component, Div, ParentComponent, Render, Styled, Subscription, View, ViewContext, - WindowContext, + div, AnyView, Component, Div, ParentElement, Render, Styled, Subscription, View, + ViewContext, WindowContext, }; use theme2::ActiveTheme; use ui::h_stack; diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 08d248f6f2..af61091ce4 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -31,10 +31,10 @@ use futures::{ use gpui::{ actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, - FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model, - ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, - Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, - WindowHandle, WindowOptions, + FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext, + ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; From 0673606de86715e4fb2d965cbb9c07b2c6af55b7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 18 Nov 2023 20:22:43 -0700 Subject: [PATCH 023/205] WIP --- crates/editor2/src/editor.rs | 2 +- crates/editor2/src/element.rs | 10 ++++++---- crates/storybook3/src/storybook3.rs | 2 +- crates/workspace2/src/dock.rs | 10 +++++----- crates/workspace2/src/modal_layer.rs | 2 +- crates/workspace2/src/notifications.rs | 4 ++-- crates/workspace2/src/pane.rs | 10 +++++----- crates/workspace2/src/pane_group.rs | 18 ++++++++++-------- crates/workspace2/src/status_bar.rs | 10 +++++----- crates/workspace2/src/toolbar.rs | 4 ++-- crates/workspace2/src/workspace2.rs | 2 +- 11 files changed, 39 insertions(+), 35 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 214c46551f..b241b76184 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9376,7 +9376,7 @@ impl FocusableView for Editor { } } -impl Render for Editor { +impl Render for Editor { type Element = EditorElement; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 0ee710da47..3140008bc3 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -21,7 +21,7 @@ use gpui::{ div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, LineLayout, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine, }; @@ -2466,9 +2466,11 @@ impl Element for EditorElement { } } -impl Component for EditorElement { - fn render(self) -> AnyElement { - AnyElement::new(self) +impl RenderOnce for EditorElement { + type Element = Self; + + fn render_once(self) -> Self::Element { + self } } diff --git a/crates/storybook3/src/storybook3.rs b/crates/storybook3/src/storybook3.rs index 291f8ce2ac..e8f1827e01 100644 --- a/crates/storybook3/src/storybook3.rs +++ b/crates/storybook3/src/storybook3.rs @@ -59,7 +59,7 @@ struct TestView { story: AnyView, } -impl Render for TestView { +impl Render for TestView { type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 406a7a33d1..d6b8d9ae79 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,8 +1,8 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId, - EventEmitter, FocusHandle, FocusableView, ParentElement, Render, SharedString, Styled, - Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, + EventEmitter, FocusHandle, FocusableView, ParentElement, Render, RenderOnce, SharedString, + Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -426,7 +426,7 @@ impl Dock { } } -impl Render for Dock { +impl Render for Dock { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -612,7 +612,7 @@ impl PanelButtons { // } // here be kittens -impl Render for PanelButtons { +impl Render for PanelButtons { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -706,7 +706,7 @@ pub mod test { } } - impl Render for TestPanel { + impl Render for TestPanel { type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index 8afd8317f9..3cbd8458f4 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -71,7 +71,7 @@ impl ModalLayer { } } -impl Render for ModalLayer { +impl Render for ModalLayer { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/workspace2/src/notifications.rs b/crates/workspace2/src/notifications.rs index 7277cc6fc4..bb00973d5c 100644 --- a/crates/workspace2/src/notifications.rs +++ b/crates/workspace2/src/notifications.rs @@ -13,7 +13,7 @@ pub enum NotificationEvent { Dismiss, } -pub trait Notification: EventEmitter + Render {} +pub trait Notification: EventEmitter + Render {} pub trait NotificationHandle: Send { fn id(&self) -> EntityId; @@ -251,7 +251,7 @@ pub mod simple_message_notification { // } } - impl Render for MessageNotification { + impl Render for MessageNotification { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index d44d347114..4ab78d8bca 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -593,7 +593,7 @@ impl Pane { self.items.iter() } - pub fn items_of_type(&self) -> impl '_ + Iterator> { + pub fn items_of_type>(&self) -> impl '_ + Iterator> { self.items .iter() .filter_map(|item| item.to_any().downcast().ok()) @@ -1343,7 +1343,7 @@ impl Pane { item: &Box, detail: usize, cx: &mut ViewContext<'_, Pane>, - ) -> impl Component { + ) -> impl RenderOnce { let label = item.tab_content(Some(detail), cx); let close_icon = || { let id = item.item_id(); @@ -1436,7 +1436,7 @@ impl Pane { ) } - fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl Component { + fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl RenderOnce { div() .group("tab_bar") .id("tab_bar") @@ -1895,7 +1895,7 @@ impl FocusableView for Pane { } } -impl Render for Pane { +impl Render for Pane { type Element = Focusable>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -2949,7 +2949,7 @@ struct DraggedTab { title: String, } -impl Render for DraggedTab { +impl Render for DraggedTab { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index d46757ff9f..68f26295dc 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -6,7 +6,10 @@ use db2::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use gpui::{point, size, AnyElement, AnyWeakView, Bounds, Model, Pixels, Point, View, ViewContext}; +use gpui::{ + point, size, AnyElement, AnyWeakView, Bounds, Div, Model, Pixels, Point, RenderOnce, View, + ViewContext, +}; use parking_lot::Mutex; use project2::Project; use serde::Deserialize; @@ -130,7 +133,7 @@ impl PaneGroup { zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> impl Component { + ) -> impl RenderOnce { self.root.render( project, 0, @@ -202,7 +205,7 @@ impl Member { zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> impl Component { + ) -> impl RenderOnce { match self { Member::Pane(pane) => { // todo!() @@ -212,7 +215,7 @@ impl Member { // Some(pane) // }; - div().size_full().child(pane.clone()).render() + div().size_full().child(pane.clone()) // Stack::new() // .with_child(pane_element.contained().with_border(leader_border)) @@ -559,7 +562,7 @@ impl PaneAxis { zoomed: Option<&AnyWeakView>, app_state: &Arc, cx: &mut ViewContext, - ) -> AnyElement { + ) -> Div { debug_assert!(self.members.len() == self.flexes.lock().len()); div() @@ -582,11 +585,10 @@ impl PaneAxis { app_state, cx, ) - .render(), - Member::Pane(pane) => pane.clone().render(), + .render_into_any(), + Member::Pane(pane) => pane.clone().render_into_any(), } })) - .render() // let mut pane_axis = PaneAxisElement::new( // self.axis, diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index b0f905f845..f961cac8f0 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -2,14 +2,14 @@ use std::any::TypeId; use crate::{ItemHandle, Pane}; use gpui::{ - div, AnyView, Component, Div, ParentElement, Render, Styled, Subscription, View, + div, AnyView, Component, Div, ParentElement, Render, RenderOnce, Styled, Subscription, View, ViewContext, WindowContext, }; use theme2::ActiveTheme; use ui::h_stack; use util::ResultExt; -pub trait StatusItemView: Render { +pub trait StatusItemView: Render { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn crate::ItemHandle>, @@ -34,7 +34,7 @@ pub struct StatusBar { _observe_active_pane: Subscription, } -impl Render for StatusBar { +impl Render for StatusBar { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -53,14 +53,14 @@ impl Render for StatusBar { } impl StatusBar { - fn render_left_tools(&self, cx: &mut ViewContext) -> impl Component { + fn render_left_tools(&self, cx: &mut ViewContext) -> impl RenderOnce { h_stack() .items_center() .gap_1() .children(self.left_items.iter().map(|item| item.to_any())) } - fn render_right_tools(&self, cx: &mut ViewContext) -> impl Component { + fn render_right_tools(&self, cx: &mut ViewContext) -> impl RenderOnce { h_stack() .items_center() .gap_2() diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 1d67da06b2..072c67fda8 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -7,7 +7,7 @@ pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), } -pub trait ToolbarItemView: Render + EventEmitter { +pub trait ToolbarItemView: Render + EventEmitter { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn crate::ItemHandle>, @@ -51,7 +51,7 @@ pub struct Toolbar { items: Vec<(Box, ToolbarItemLocation)>, } -impl Render for Toolbar { +impl Render for Toolbar { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index af61091ce4..e6b3f1875f 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3592,7 +3592,7 @@ impl FocusableView for Workspace { } } -impl Render for Workspace { +impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { From 33cd6f520a32a271963d818794169d9e14858ef9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 18 Nov 2023 21:51:47 -0700 Subject: [PATCH 024/205] Clean compile with redesigned element traits --- crates/collab_ui2/src/collab_panel.rs | 2 +- crates/collab_ui2/src/collab_titlebar_item.rs | 4 +- .../command_palette2/src/command_palette.rs | 4 +- crates/editor2/src/editor.rs | 35 ++--- crates/editor2/src/editor_tests.rs | 2 +- crates/editor2/src/element.rs | 39 +++--- crates/file_finder2/src/file_finder.rs | 7 +- crates/go_to_line2/src/go_to_line.rs | 2 +- crates/gpui2/src/element.rs | 130 ++++++++++++------ crates/gpui2/src/elements/div.rs | 24 ++-- crates/gpui2/src/elements/img.rs | 8 +- crates/gpui2/src/elements/overlay.rs | 8 +- crates/gpui2/src/elements/svg.rs | 8 +- crates/gpui2/src/elements/text.rs | 32 ++--- crates/gpui2/src/elements/uniform_list.rs | 8 +- crates/gpui2/src/view.rs | 24 ++-- crates/gpui2_macros/src/derive_render_once.rs | 4 + crates/picker2/src/picker2.rs | 8 +- crates/project_panel2/src/project_panel.rs | 8 +- crates/storybook2/src/stories/colors.rs | 4 +- crates/storybook2/src/stories/focus.rs | 2 +- crates/storybook2/src/stories/kitchen_sink.rs | 2 +- crates/storybook2/src/stories/picker.rs | 9 +- crates/storybook2/src/stories/scroll.rs | 2 +- crates/storybook2/src/stories/text.rs | 6 +- crates/storybook2/src/stories/z_index.rs | 24 ++-- crates/storybook2/src/storybook2.rs | 2 +- crates/terminal_view2/src/terminal_panel.rs | 2 +- crates/terminal_view2/src/terminal_view.rs | 10 +- crates/ui2/src/components/context_menu.rs | 31 ++--- crates/ui2/src/components/divider.rs | 21 ++- crates/workspace2/src/dock.rs | 9 +- crates/workspace2/src/pane.rs | 6 +- crates/workspace2/src/pane_group.rs | 3 +- crates/workspace2/src/status_bar.rs | 4 +- 35 files changed, 278 insertions(+), 216 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 22f6dc9d88..901348d2e2 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -3294,7 +3294,7 @@ impl CollabPanel { // .with_width(size.x()) // } -impl Render for CollabPanel { +impl Render for CollabPanel { type Element = Focusable>; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 570ff9f45e..42800269c7 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,7 +31,7 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, px, rems, AppContext, Component, Div, InteractiveElement, Model, ParentElement, Render, + div, px, rems, AppContext, Div, InteractiveElement, Model, ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView, WindowBounds, }; @@ -81,7 +81,7 @@ pub struct CollabTitlebarItem { _subscriptions: Vec, } -impl Render for CollabTitlebarItem { +impl Render for CollabTitlebarItem { type Element = Stateful>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 7e4994638e..cf3be8379a 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,7 +1,7 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke, + actions, div, prelude::*, Action, AppContext, Dismiss, Div, FocusHandle, Keystroke, ManagedView, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; @@ -74,7 +74,7 @@ impl ManagedView for CommandPalette { } } -impl Render for CommandPalette { +impl Render for CommandPalette { type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index b241b76184..6e9f0f0b6e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -42,9 +42,9 @@ use gpui::{ actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, - Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, Styled, - Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, - WeakView, WindowContext, + Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, + SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -1580,7 +1580,8 @@ impl CodeActionsMenu { ) .map(|task| task.detach_and_log_err(cx)); }) - .child(action.lsp_action.title.clone()) + // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. + .child(SharedString::from(action.lsp_action.title.clone())) }) .collect() }, @@ -1595,7 +1596,7 @@ impl CodeActionsMenu { .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) .map(|(ix, _)| ix), ) - .render_once(); + .render_into_any(); if self.deployed_from_indicator { *cursor_position.column_mut() = 0; @@ -4353,19 +4354,19 @@ impl Editor { style: &EditorStyle, is_active: bool, cx: &mut ViewContext, - ) -> Option> { + ) -> Option> { if self.available_code_actions.is_some() { Some( - IconButton::new("code_actions_indicator", ui::Icon::Bolt) - .on_click(|editor: &mut Editor, cx| { + IconButton::new("code_actions_indicator", ui::Icon::Bolt).on_click( + |editor: &mut Editor, cx| { editor.toggle_code_actions( &ToggleCodeActions { deployed_from_indicator: true, }, cx, ); - }) - .into_any(), + }, + ), ) } else { None @@ -4380,7 +4381,7 @@ impl Editor { line_height: Pixels, gutter_margin: Pixels, cx: &mut ViewContext, - ) -> Vec>> { + ) -> Vec>> { fold_data .iter() .enumerate() @@ -4392,16 +4393,16 @@ impl Editor { FoldStatus::Folded => ui::Icon::ChevronRight, FoldStatus::Foldable => ui::Icon::ChevronDown, }; - IconButton::new(ix as usize, icon) - .on_click(move |editor: &mut Editor, cx| match fold_status { + IconButton::new(ix as usize, icon).on_click( + move |editor: &mut Editor, cx| match fold_status { FoldStatus::Folded => { editor.unfold_at(&UnfoldAt { buffer_row }, cx); } FoldStatus::Foldable => { editor.fold_at(&FoldAt { buffer_row }, cx); } - }) - .into_any() + }, + ) }) }) .flatten() @@ -7792,7 +7793,7 @@ impl Editor { cx.editor_style.diagnostic_style.clone(), }, ))) - .render_once() + .render_into_any() } }), disposition: BlockDisposition::Below, @@ -9994,7 +9995,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend cx.write_to_clipboard(ClipboardItem::new(message.clone())); }) .tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx)) - .render_once() + .render_into_any() }) } diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index f23618aa21..e27f13cd59 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { position: snapshot.anchor_after(Point::new(2, 0)), disposition: BlockDisposition::Below, height: 1, - render: Arc::new(|_| div().render_once()), + render: Arc::new(|_| div().into_any()), }], Some(Autoscroll::fit()), cx, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3140008bc3..a207e064c5 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -490,6 +490,7 @@ impl EditorElement { for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { if let Some(mut fold_indicator) = fold_indicator { + let mut fold_indicator = fold_indicator.render_into_any(); let available_space = size( AvailableSpace::MinContent, AvailableSpace::Definite(line_height * 0.55), @@ -509,20 +510,21 @@ impl EditorElement { } } - if let Some(mut indicator) = layout.code_actions_indicator.take() { + if let Some(indicator) = layout.code_actions_indicator.take() { + let mut button = indicator.button.render_into_any(); let available_space = size( AvailableSpace::MinContent, AvailableSpace::Definite(line_height), ); - let indicator_size = indicator.element.measure(available_space, editor, cx); + let indicator_size = button.measure(available_space, editor, cx); + let mut x = Pixels::ZERO; let mut y = indicator.row as f32 * line_height - scroll_top; // Center indicator. x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; y += (line_height - indicator_size.height) / 2.; - indicator - .element - .draw(bounds.origin + point(x, y), available_space, editor, cx); + + button.draw(bounds.origin + point(x, y), available_space, editor, cx); } } @@ -1810,7 +1812,7 @@ impl EditorElement { .render_code_actions_indicator(&style, active, cx) .map(|element| CodeActionsIndicator { row: newest_selection_head.row(), - element, + button: element, }); } } @@ -2041,14 +2043,19 @@ impl EditorElement { // Can't use .and_then() because `.file_name()` and `.parent()` return references :( if let Some(path) = path { filename = path.file_name().map(|f| f.to_string_lossy().to_string()); - parent_path = - path.parent().map(|p| p.to_string_lossy().to_string() + "/"); + parent_path = path + .parent() + .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/")); } h_stack() .size_full() .bg(gpui::red()) - .child(filename.unwrap_or_else(|| "untitled".to_string())) + .child( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + ) .children(parent_path) .children(jump_icon) // .p_x(gutter_padding) } else { @@ -2059,7 +2066,7 @@ impl EditorElement { .child("⋯") .children(jump_icon) // .p_x(gutter_padding) }; - element.render() + element.into_any() } }; @@ -2391,10 +2398,6 @@ enum Invisible { impl Element for EditorElement { type State = (); - fn element_id(&self) -> Option { - Some(self.editor_id.into()) - } - fn layout( &mut self, editor: &mut Editor, @@ -2469,6 +2472,10 @@ impl Element for EditorElement { impl RenderOnce for EditorElement { type Element = Self; + fn element_id(&self) -> Option { + Some(self.editor_id.into()) + } + fn render_once(self) -> Self::Element { self } @@ -3098,14 +3105,14 @@ pub struct LayoutState { context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option, // hover_popovers: Option<(DisplayPoint, Vec>)>, - fold_indicators: Vec>>, + fold_indicators: Vec>>, tab_invisible: ShapedLine, space_invisible: ShapedLine, } struct CodeActionsIndicator { row: u32, - element: AnyElement, + button: IconButton, } struct PositionMap { diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 3488b6916c..77bc14fa96 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,9 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveElement, - ManagedView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, - WeakView, + actions, div, AppContext, Dismiss, Div, FocusHandle, InteractiveElement, ManagedView, Model, + ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -116,7 +115,7 @@ impl ManagedView for FileFinder { self.picker.focus_handle(cx) } } -impl Render for FileFinder { +impl Render for FileFinder { type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 26641bfa09..81e1d990f1 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -143,7 +143,7 @@ impl GoToLine { } } -impl Render for GoToLine { +impl Render for GoToLine { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index c60c902e25..990de68b06 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -14,12 +14,50 @@ pub trait Render: 'static + Sized { pub trait RenderOnce: Sized { type Element: Element + 'static; + fn element_id(&self) -> Option; + fn render_once(self) -> Self::Element; fn render_into_any(self) -> AnyElement { self.render_once().into_any() } + fn draw( + self, + origin: Point, + available_space: Size, + view_state: &mut V, + cx: &mut ViewContext, + f: impl FnOnce(&mut >::State, &mut ViewContext) -> R, + ) -> R + where + T: Clone + Default + Debug + Into, + { + let element = self.render_once(); + let element_id = element.element_id(); + let element = DrawableElement { + element: Some(element), + phase: ElementDrawPhase::Start, + }; + let frame_state = DrawableElement::draw( + element, + origin, + available_space.map(Into::into), + view_state, + cx, + ); + + if let Some(mut frame_state) = frame_state { + f(&mut frame_state, cx) + } else { + cx.with_element_state(element_id.unwrap(), |element_state, cx| { + let mut element_state = element_state.unwrap(); + let result = f(&mut element_state, cx); + (result, element_state) + }) + } + } + fn map(self, f: impl FnOnce(Self) -> U) -> U where Self: Sized, @@ -52,8 +90,6 @@ pub trait RenderOnce: Sized { pub trait Element: 'static + RenderOnce { type State: 'static; - fn element_id(&self) -> Option; - fn layout( &mut self, view_state: &mut V, @@ -72,35 +108,6 @@ pub trait Element: 'static + RenderOnce { fn into_any(self) -> AnyElement { AnyElement::new(self) } - - fn draw( - self, - origin: Point, - available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - f: impl FnOnce(&mut Self::State, &mut ViewContext) -> R, - ) -> R - where - T: Clone + Default + Debug + Into, - { - let element_id = self.element_id(); - let element = DrawableElement { - element: Some(self), - phase: ElementDrawPhase::Start, - }; - let frame_state = element.draw(origin, available_space.map(Into::into), view_state, cx); - - if let Some(mut frame_state) = frame_state { - f(&mut frame_state, cx) - } else { - cx.with_element_state(element_id.unwrap(), |element_state, cx| { - let mut element_state = element_state.unwrap(); - let result = f(&mut element_state, cx); - (result, element_state) - }) - } - } } pub trait Component: 'static { @@ -131,10 +138,6 @@ impl CompositeElement { impl> Element for CompositeElement { type State = CompositeElementState; - fn element_id(&self) -> Option { - None - } - fn layout( &mut self, view: &mut V, @@ -174,6 +177,10 @@ impl> Element for CompositeElement { impl> RenderOnce for CompositeElement { type Element = Self; + fn element_id(&self) -> Option { + None + } + fn render_once(self) -> Self::Element { self } @@ -231,23 +238,21 @@ pub struct DrawableElement> { } #[derive(Default)] -enum ElementDrawPhase { +enum ElementDrawPhase { #[default] Start, LayoutRequested { layout_id: LayoutId, - frame_state: Option, + frame_state: Option, }, LayoutComputed { layout_id: LayoutId, available_space: Size, - frame_state: Option, + frame_state: Option, }, } -/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered. -/// It's allocated as a trait object to erase the element type and wrapped in AnyElement for -/// improved usability. +/// A wrapper around an implementer of [Element] that allows it to be drawn in a window. impl> DrawableElement { fn new(element: E) -> Self { DrawableElement { @@ -379,6 +384,41 @@ impl> DrawableElement { } } +// impl> Element for DrawableElement { +// type State = >::State; + +// fn layout( +// &mut self, +// view_state: &mut V, +// element_state: Option, +// cx: &mut ViewContext, +// ) -> (LayoutId, Self::State) { + +// } + +// fn paint( +// self, +// bounds: Bounds, +// view_state: &mut V, +// element_state: &mut Self::State, +// cx: &mut ViewContext, +// ) { +// todo!() +// } +// } + +// impl> RenderOnce for DrawableElement { +// type Element = Self; + +// fn element_id(&self) -> Option { +// self.element.as_ref()?.element_id() +// } + +// fn render_once(self) -> Self::Element { +// self +// } +// } + impl ElementObject for Option> where E: Element, @@ -476,10 +516,6 @@ impl AnyElement { impl Element for AnyElement { type State = (); - fn element_id(&self) -> Option { - AnyElement::element_id(self) - } - fn layout( &mut self, view_state: &mut V, @@ -504,6 +540,10 @@ impl Element for AnyElement { impl RenderOnce for AnyElement { type Element = Self; + fn element_id(&self) -> Option { + AnyElement::element_id(self) + } + fn render_once(self) -> Self::Element { self } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 26de4ea25f..256791ed04 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -602,10 +602,6 @@ impl ParentElement for Div { impl Element for Div { type State = DivState; - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } - fn layout( &mut self, view_state: &mut V, @@ -694,6 +690,10 @@ impl Element for Div { impl RenderOnce for Div { type Element = Self; + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() + } + fn render_once(self) -> Self::Element { self } @@ -1293,10 +1293,6 @@ where { type State = E::State; - fn element_id(&self) -> Option { - self.element.element_id() - } - fn layout( &mut self, view_state: &mut V, @@ -1324,6 +1320,10 @@ where { type Element = Self; + fn element_id(&self) -> Option { + self.element.element_id() + } + fn render_once(self) -> Self::Element { self } @@ -1381,10 +1381,6 @@ where { type State = E::State; - fn element_id(&self) -> Option { - self.element.element_id() - } - fn layout( &mut self, view_state: &mut V, @@ -1412,6 +1408,10 @@ where { type Element = Self; + fn element_id(&self) -> Option { + self.element.element_id() + } + fn render_once(self) -> Self::Element { self } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 16f20869ab..b4c439d0cb 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -37,10 +37,6 @@ where impl Element for Img { type State = InteractiveElementState; - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } - fn layout( &mut self, _view_state: &mut V, @@ -98,6 +94,10 @@ impl Element for Img { impl RenderOnce for Img { type Element = Self; + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() + } + fn render_once(self) -> Self::Element { self } diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 4e01b8aad1..79a3643bdc 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -60,10 +60,6 @@ impl ParentElement for Overlay { impl Element for Overlay { type State = OverlayState; - fn element_id(&self) -> Option { - None - } - fn layout( &mut self, view_state: &mut V, @@ -160,6 +156,10 @@ impl Element for Overlay { impl RenderOnce for Overlay { type Element = Self; + fn element_id(&self) -> Option { + None + } + fn render_once(self) -> Self::Element { self } diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index f6823c50d5..0940bdd1eb 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -26,10 +26,6 @@ impl Svg { impl Element for Svg { type State = InteractiveElementState; - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } - fn layout( &mut self, _view_state: &mut V, @@ -62,6 +58,10 @@ impl Element for Svg { impl RenderOnce for Svg { type Element = Self; + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() + } + fn render_once(self) -> Self::Element { self } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index bfd63d3c3d..9c7cd35e25 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -11,10 +11,6 @@ use util::ResultExt; impl Element for &'static str { type State = TextState; - fn element_id(&self) -> Option { - None - } - fn layout( &mut self, _: &mut V, @@ -40,6 +36,10 @@ impl Element for &'static str { impl RenderOnce for &'static str { type Element = Self; + fn element_id(&self) -> Option { + None + } + fn render_once(self) -> Self::Element { self } @@ -48,10 +48,6 @@ impl RenderOnce for &'static str { impl Element for SharedString { type State = TextState; - fn element_id(&self) -> Option { - Some(self.clone().into()) - } - fn layout( &mut self, _: &mut V, @@ -78,6 +74,10 @@ impl Element for SharedString { impl RenderOnce for SharedString { type Element = Self; + fn element_id(&self) -> Option { + Some(self.clone().into()) + } + fn render_once(self) -> Self::Element { self } @@ -105,10 +105,6 @@ impl StyledText { impl Element for StyledText { type State = TextState; - fn element_id(&self) -> Option { - None - } - fn layout( &mut self, _view: &mut V, @@ -194,6 +190,10 @@ impl Element for StyledText { impl RenderOnce for StyledText { type Element = Self; + fn element_id(&self) -> Option { + None + } + fn render_once(self) -> Self::Element { self } @@ -300,10 +300,6 @@ struct InteractiveTextState { impl Element for InteractiveText { type State = InteractiveTextState; - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - fn layout( &mut self, view_state: &mut V, @@ -346,6 +342,10 @@ impl Element for InteractiveText { impl RenderOnce for InteractiveText { type Element = Self; + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + fn render_once(self) -> Self::Element { self } diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 997904d913..caf18962ec 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -104,10 +104,6 @@ pub struct UniformListState { impl Element for UniformList { type State = UniformListState; - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - fn layout( &mut self, view_state: &mut V, @@ -255,6 +251,10 @@ impl Element for UniformList { impl RenderOnce for UniformList { type Element = Self; + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + fn render_once(self) -> Self::Element { self } diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index c32bc70e4a..6d6863b54b 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -208,10 +208,6 @@ impl> From> for AnyView { impl, ParentV: 'static> Element for View { type State = Option>; - fn element_id(&self) -> Option { - Some(self.model.entity_id.into()) - } - fn layout( &mut self, _parent_view: &mut ParentV, @@ -241,6 +237,10 @@ impl, ParentV: 'static> Element for View { impl, ParentV: 'static> RenderOnce for View { type Element = View; + fn element_id(&self) -> Option { + Some(self.model.entity_id.into()) + } + fn render_once(self) -> Self::Element { self } @@ -249,10 +249,6 @@ impl, ParentV: 'static> RenderOnce for View { impl Element for AnyView { type State = Option>; - fn element_id(&self) -> Option { - Some(self.model.entity_id.into()) - } - fn layout( &mut self, _view_state: &mut V, @@ -277,6 +273,10 @@ impl Element for AnyView { impl RenderOnce for AnyView { type Element = Self; + fn element_id(&self) -> Option { + Some(self.model.entity_id.into()) + } + fn render_once(self) -> Self::Element { self } @@ -334,10 +334,6 @@ where { type State = Option>; - fn element_id(&self) -> Option { - Some(self.view.entity_id().into()) - } - fn layout( &mut self, _: &mut ParentV, @@ -371,6 +367,10 @@ where { type Element = Self; + fn element_id(&self) -> Option { + self.element.as_ref().unwrap().element_id() + } + fn render_once(self) -> Self::Element { self } diff --git a/crates/gpui2_macros/src/derive_render_once.rs b/crates/gpui2_macros/src/derive_render_once.rs index ee01e22358..d1dcfc5dc1 100644 --- a/crates/gpui2_macros/src/derive_render_once.rs +++ b/crates/gpui2_macros/src/derive_render_once.rs @@ -33,6 +33,10 @@ pub fn derive_render_once(input: TokenStream) -> TokenStream { { type Element = gpui::CompositeElement<#view_type, Self>; + fn element_id(&self) -> Option { + None + } + fn render_once(self) -> Self::Element { gpui::CompositeElement::new(self) } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 3491fc3d4a..ef4ded65ea 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,7 @@ use editor::Editor; use gpui::{ - div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView, - MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, + div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton, + Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Divider, Label, TextColor}; @@ -15,7 +15,7 @@ pub struct Picker { } pub trait PickerDelegate: Sized + 'static { - type ListItem: Component>; + type ListItem: RenderOnce>; fn match_count(&self) -> usize; fn selected_index(&self) -> usize; @@ -180,7 +180,7 @@ impl Picker { } } -impl Render for Picker { +impl Render for Picker { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 46ad7a7a41..7fbb6cd553 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -9,9 +9,9 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, - ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView, - InteractiveElement, Model, MouseButton, ParentElement, Pixels, Point, PromptLevel, Render, - Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, ViewContext, + ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, + Model, MouseButton, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, Stateful, + StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; @@ -1423,7 +1423,7 @@ impl ProjectPanel { } } -impl Render for ProjectPanel { +impl Render for ProjectPanel { type Element = Focusable>>; fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { diff --git a/crates/storybook2/src/stories/colors.rs b/crates/storybook2/src/stories/colors.rs index 4f8c54fa6f..b690435e01 100644 --- a/crates/storybook2/src/stories/colors.rs +++ b/crates/storybook2/src/stories/colors.rs @@ -5,7 +5,7 @@ use ui::prelude::*; pub struct ColorsStory; -impl Render for ColorsStory { +impl Render for ColorsStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -28,7 +28,7 @@ impl Render for ColorsStory { div() .w(px(75.)) .line_height(px(24.)) - .child(scale.name().to_string()), + .child(scale.name().clone()), ) .child( div() diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 571882f1f2..12c7ea81a0 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -26,7 +26,7 @@ impl FocusStory { } } -impl Render for FocusStory { +impl Render for FocusStory { type Element = Focusable>>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index 507aa8db2d..2d31cefed6 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -11,7 +11,7 @@ impl KitchenSinkStory { } } -impl Render for KitchenSinkStory { +impl Render for KitchenSinkStory { type Element = Stateful>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index a3f9ef5eb8..7c2412a02f 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,5 +1,7 @@ use fuzzy::StringMatchCandidate; -use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext}; +use gpui::{ + div, prelude::*, Div, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext, +}; use picker::{Picker, PickerDelegate}; use std::sync::Arc; use theme2::ActiveTheme; @@ -54,7 +56,8 @@ impl PickerDelegate for Delegate { let Some(candidate_ix) = self.matches.get(ix) else { return div(); }; - let candidate = self.candidates[*candidate_ix].string.clone(); + // TASK: Make StringMatchCandidate::string a SharedString + let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone()); div() .text_color(colors.text) @@ -202,7 +205,7 @@ impl PickerStory { } } -impl Render for PickerStory { +impl Render for PickerStory { type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index f1bb7b4e7c..bbab0b1d11 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -10,7 +10,7 @@ impl ScrollStory { } } -impl Render for ScrollStory { +impl Render for ScrollStory { type Element = Stateful>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index 94c9d0d51f..722832c76a 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,6 +1,4 @@ -use gpui::{ - div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext, -}; +use gpui::{div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext}; pub struct TextStory; @@ -10,7 +8,7 @@ impl TextStory { } } -impl Render for TextStory { +impl Render for TextStory { type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 4916f192b1..087ed913fd 100644 --- a/crates/storybook2/src/stories/z_index.rs +++ b/crates/storybook2/src/stories/z_index.rs @@ -1,4 +1,4 @@ -use gpui::{px, rgb, Div, Hsla, Render}; +use gpui::{px, rgb, Div, Hsla, Render, RenderOnce}; use ui::prelude::*; use crate::story::Story; @@ -7,7 +7,7 @@ use crate::story::Story; /// [https://developer.mozilla.org/en-US/docs/Web/CSS/z-index](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index). pub struct ZIndexStory; -impl Render for ZIndexStory { +impl Render for ZIndexStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -79,17 +79,15 @@ trait Styles: Styled + Sized { impl Styles for Div {} -// #[derive(RenderOnce)] +#[derive(RenderOnce)] struct ZIndexExample { z_index: u32, } -impl ZIndexExample { - pub fn new(z_index: u32) -> Self { - Self { z_index } - } +impl Component for ZIndexExample { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { div() .relative() .size_full() @@ -109,14 +107,14 @@ impl ZIndexExample { // HACK: Simulate `text-align: center`. .pl(px(24.)) .z_index(self.z_index) - .child(format!( + .child(SharedString::from(format!( "z-index: {}", if self.z_index == 0 { "auto".to_string() } else { self.z_index.to_string() } - )), + ))), ) // Blue blocks. .child( @@ -173,3 +171,9 @@ impl ZIndexExample { ) } } + +impl ZIndexExample { + pub fn new(z_index: u32) -> Self { + Self { z_index } + } +} diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index a0bc7cd72f..2a22d91382 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -105,7 +105,7 @@ impl StoryWrapper { } } -impl Render for StoryWrapper { +impl Render for StoryWrapper { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index 6321e61e35..46885913ed 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/crates/terminal_view2/src/terminal_panel.rs @@ -335,7 +335,7 @@ impl TerminalPanel { impl EventEmitter for TerminalPanel {} -impl Render for TerminalPanel { +impl Render for TerminalPanel { type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 8f5aeb5630..079f2b9f88 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -9,7 +9,7 @@ pub mod terminal_panel; // use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div, + actions, div, img, red, Action, AnyElement, AppContext, DispatchPhase, Div, Element, EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, InteractiveElement, KeyDownEvent, Keystroke, Model, MouseButton, ParentElement, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, @@ -538,7 +538,7 @@ impl TerminalView { } } -impl Render for TerminalView { +impl Render for TerminalView { type Element = Focusable>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -578,7 +578,7 @@ impl Render for TerminalView { .children( self.context_menu .clone() - .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())), + .map(|context_menu| div().z_index(1).absolute().child(context_menu)), ) .track_focus(&self.focus_handle) .on_focus_in(Self::focus_in) @@ -756,8 +756,8 @@ impl Item for TerminalView { div() .child(img().uri("icons/terminal.svg").bg(red())) - .child(title) - .render() + .child(SharedString::from(title)) + .into_any() } fn clone_on_split( diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 41675e6bcd..6235704d7e 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -78,7 +78,7 @@ impl Render for ContextMenu { } pub struct MenuHandle { - id: Option, + id: ElementId, child_builder: Option AnyElement + 'static>>, menu_builder: Option) -> View + 'static>>, @@ -87,11 +87,6 @@ pub struct MenuHandle { } impl MenuHandle { - pub fn id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext) -> View + 'static) -> Self { self.menu_builder = Some(Rc::new(f)); self @@ -116,9 +111,9 @@ impl MenuHandle { } } -pub fn menu_handle() -> MenuHandle { +pub fn menu_handle(id: impl Into) -> MenuHandle { MenuHandle { - id: None, + id: id.into(), child_builder: None, menu_builder: None, anchor: None, @@ -136,10 +131,6 @@ pub struct MenuHandleState { impl Element for MenuHandle { type State = MenuHandleState; - fn element_id(&self) -> Option { - Some(self.id.clone().expect("menu_handle must have an id()")) - } - fn layout( &mut self, view_state: &mut V, @@ -251,6 +242,10 @@ impl Element for MenuHandle { impl RenderOnce for MenuHandle { type Element = Self; + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + fn render_once(self) -> Self::Element { self } @@ -297,8 +292,7 @@ mod stories { .flex_col() .justify_between() .child( - menu_handle() - .id("test2") + menu_handle("test2") .child(|is_open| { Label::new(if is_open { "TOP LEFT" @@ -309,8 +303,7 @@ mod stories { .menu(move |_, cx| build_menu(cx, "top left")), ) .child( - menu_handle() - .id("test1") + menu_handle("test1") .child(|is_open| { Label::new(if is_open { "BOTTOM LEFT" @@ -329,8 +322,7 @@ mod stories { .flex_col() .justify_between() .child( - menu_handle() - .id("test3") + menu_handle("test3") .child(|is_open| { Label::new(if is_open { "TOP RIGHT" @@ -342,8 +334,7 @@ mod stories { .menu(move |_, cx| build_menu(cx, "top right")), ) .child( - menu_handle() - .id("test4") + menu_handle("test4") .child(|is_open| { Label::new(if is_open { "BOTTOM RIGHT" diff --git a/crates/ui2/src/components/divider.rs b/crates/ui2/src/components/divider.rs index a651ab08af..206f2e26db 100644 --- a/crates/ui2/src/components/divider.rs +++ b/crates/ui2/src/components/divider.rs @@ -1,4 +1,4 @@ -use gpui::RenderOnce; +use gpui::{Div, RenderOnce}; use crate::prelude::*; @@ -7,12 +7,29 @@ enum DividerDirection { Vertical, } -// #[derive(RenderOnce)] +#[derive(RenderOnce)] pub struct Divider { direction: DividerDirection, inset: bool, } +impl Component for Divider { + type Rendered = Div; + + fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + div() + .map(|this| match self.direction { + DividerDirection::Horizontal => { + this.h_px().w_full().when(self.inset, |this| this.mx_1p5()) + } + DividerDirection::Vertical => { + this.w_px().h_full().when(self.inset, |this| this.my_1p5()) + } + }) + .bg(cx.theme().colors().border_variant) + } +} + impl Divider { pub fn horizontal() -> Self { Self { diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index d6b8d9ae79..f143b2c2e5 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,8 +1,8 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId, - EventEmitter, FocusHandle, FocusableView, ParentElement, Render, RenderOnce, SharedString, - Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, + div, px, Action, AnchorCorner, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, + FocusHandle, FocusableView, ParentElement, Render, RenderOnce, SharedString, Styled, + Subscription, View, ViewContext, VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -653,8 +653,7 @@ impl Render for PanelButtons { }; Some( - menu_handle() - .id(name) + menu_handle(name) .menu(move |_, cx| { cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")) }) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 4ab78d8bca..50eaa043ed 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -7,9 +7,9 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId, - EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, - Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, prelude::*, Action, AppContext, AsyncWindowContext, Div, EntityId, EventEmitter, + FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; diff --git a/crates/workspace2/src/pane_group.rs b/crates/workspace2/src/pane_group.rs index 68f26295dc..5d02214f9f 100644 --- a/crates/workspace2/src/pane_group.rs +++ b/crates/workspace2/src/pane_group.rs @@ -7,8 +7,7 @@ use db2::sqlez::{ statement::Statement, }; use gpui::{ - point, size, AnyElement, AnyWeakView, Bounds, Div, Model, Pixels, Point, RenderOnce, View, - ViewContext, + point, size, AnyWeakView, Bounds, Div, Model, Pixels, Point, RenderOnce, View, ViewContext, }; use parking_lot::Mutex; use project2::Project; diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index f961cac8f0..945760260c 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -2,8 +2,8 @@ use std::any::TypeId; use crate::{ItemHandle, Pane}; use gpui::{ - div, AnyView, Component, Div, ParentElement, Render, RenderOnce, Styled, Subscription, View, - ViewContext, WindowContext, + div, AnyView, Div, ParentElement, Render, RenderOnce, Styled, Subscription, View, ViewContext, + WindowContext, }; use theme2::ActiveTheme; use ui::h_stack; From 88024ca7c93d6e60fd5e1b4f90287406b01cb123 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 19 Nov 2023 18:32:31 -0800 Subject: [PATCH 025/205] Remove V parameter on elements --- crates/gpui2/src/app/test_context.rs | 2 +- crates/gpui2/src/element.rs | 144 +++--- crates/gpui2/src/elements/div.rs | 599 ++++++++++------------ crates/gpui2/src/elements/img.rs | 36 +- crates/gpui2/src/elements/overlay.rs | 30 +- crates/gpui2/src/elements/svg.rs | 28 +- crates/gpui2/src/elements/text.rs | 43 +- crates/gpui2/src/elements/uniform_list.rs | 74 ++- crates/gpui2/src/gpui2.rs | 24 + crates/gpui2/src/interactive.rs | 55 +- crates/gpui2/src/style.rs | 4 +- crates/gpui2/src/view.rs | 81 ++- crates/gpui2/src/window.rs | 123 +++-- 13 files changed, 578 insertions(+), 665 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 0315e362b0..8687646b4b 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -631,7 +631,7 @@ impl AnyWindowHandle { pub struct EmptyView {} impl Render for EmptyView { - type Element = Div; + type Element = Div; fn render(&mut self, _cx: &mut crate::ViewContext) -> Self::Element { div() diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index c773bb6f65..d36f5f1635 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -1,37 +1,34 @@ use crate::{ - AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext, + AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, WindowContext, }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; use std::{any::Any, fmt::Debug, mem}; -pub trait Element { +pub trait Element { type ElementState: 'static; fn element_id(&self) -> Option; fn layout( &mut self, - view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState); fn paint( &mut self, bounds: Bounds, - view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ); fn draw( self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - f: impl FnOnce(&Self::ElementState, &mut ViewContext) -> R, + cx: &mut WindowContext, + f: impl FnOnce(&Self::ElementState, &mut WindowContext) -> R, ) -> R where Self: Sized, @@ -41,7 +38,7 @@ pub trait Element { element: self, phase: ElementRenderPhase::Start, }; - element.draw(origin, available_space.map(Into::into), view_state, cx); + element.draw(origin, available_space.map(Into::into), cx); if let ElementRenderPhase::Painted { frame_state } = &element.phase { if let Some(frame_state) = frame_state.as_ref() { f(&frame_state, cx) @@ -65,10 +62,10 @@ pub trait Element { #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] pub struct GlobalElementId(SmallVec<[ElementId; 32]>); -pub trait ParentComponent { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; +pub trait ParentComponent { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; - fn child(mut self, child: impl Component) -> Self + fn child(mut self, child: impl Component) -> Self where Self: Sized, { @@ -76,7 +73,7 @@ pub trait ParentComponent { self } - fn children(mut self, iter: impl IntoIterator>) -> Self + fn children(mut self, iter: impl IntoIterator) -> Self where Self: Sized, { @@ -86,26 +83,24 @@ pub trait ParentComponent { } } -trait ElementObject { +trait ElementObject { fn element_id(&self) -> Option; - fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; - fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId; + fn paint(&mut self, cx: &mut WindowContext); fn measure( &mut self, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Size; fn draw( &mut self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ); } -struct RenderedElement> { +struct RenderedElement { element: E, phase: ElementRenderPhase, } @@ -131,7 +126,7 @@ enum ElementRenderPhase { /// Internal struct that wraps an element to store Layout and ElementState after the element is rendered. /// It's allocated as a trait object to erase the element type and wrapped in AnyElement for /// improved usability. -impl> RenderedElement { +impl RenderedElement { fn new(element: E) -> Self { RenderedElement { element, @@ -140,25 +135,25 @@ impl> RenderedElement { } } -impl ElementObject for RenderedElement +impl ElementObject for RenderedElement where - E: Element, + E: Element, E::ElementState: 'static, { fn element_id(&self) -> Option { self.element.element_id() } - fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { let (layout_id, frame_state) = match mem::take(&mut self.phase) { ElementRenderPhase::Start => { if let Some(id) = self.element.element_id() { let layout_id = cx.with_element_state(id, |element_state, cx| { - self.element.layout(state, element_state, cx) + self.element.layout(element_state, cx) }); (layout_id, None) } else { - let (layout_id, frame_state) = self.element.layout(state, None, cx); + let (layout_id, frame_state) = self.element.layout(None, cx); (layout_id, Some(frame_state)) } } @@ -176,7 +171,7 @@ where layout_id } - fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { + fn paint(&mut self, cx: &mut WindowContext) { self.phase = match mem::take(&mut self.phase) { ElementRenderPhase::LayoutRequested { layout_id, @@ -191,13 +186,12 @@ where if let Some(id) = self.element.element_id() { cx.with_element_state(id, |element_state, cx| { let mut element_state = element_state.unwrap(); - self.element - .paint(bounds, view_state, &mut element_state, cx); + self.element.paint(bounds, &mut element_state, cx); ((), element_state) }); } else { self.element - .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx); + .paint(bounds, frame_state.as_mut().unwrap(), cx); } ElementRenderPhase::Painted { frame_state } } @@ -209,11 +203,10 @@ where fn measure( &mut self, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Size { if matches!(&self.phase, ElementRenderPhase::Start) { - self.layout(view_state, cx); + self.layout(cx); } let layout_id = match &mut self.phase { @@ -251,21 +244,19 @@ where &mut self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - self.measure(available_space, view_state, cx); - cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx)) + self.measure(available_space, cx); + cx.with_absolute_element_offset(origin, |cx| self.paint(cx)) } } -pub struct AnyElement(Box>); +pub struct AnyElement(Box); -impl AnyElement { +impl AnyElement { pub fn new(element: E) -> Self where - V: 'static, - E: 'static + Element, + E: 'static + Element, E::ElementState: Any, { AnyElement(Box::new(RenderedElement::new(element))) @@ -275,22 +266,21 @@ impl AnyElement { self.0.element_id() } - pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { - self.0.layout(view_state, cx) + pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { + self.0.layout(cx) } - pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { - self.0.paint(view_state, cx) + pub fn paint(&mut self, cx: &mut WindowContext) { + self.0.paint(cx) } /// Initializes this element and performs layout within the given available space to determine its size. pub fn measure( &mut self, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Size { - self.0.measure(available_space, view_state, cx) + self.0.measure(available_space, cx) } /// Initializes this element and performs layout in the available space, then paints it at the given origin. @@ -298,20 +288,19 @@ impl AnyElement { &mut self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - self.0.draw(origin, available_space, view_state, cx) + self.0.draw(origin, available_space, cx) } } -pub trait Component { - fn render(self) -> AnyElement; +pub trait Component { + fn render(self) -> AnyElement; fn map(self, f: impl FnOnce(Self) -> U) -> U where Self: Sized, - U: Component, + U: Component, { f(self) } @@ -337,19 +326,18 @@ pub trait Component { } } -impl Component for AnyElement { - fn render(self) -> AnyElement { +impl Component for AnyElement { + fn render(self) -> AnyElement { self } } -impl Element for Option +impl Element for Option where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, + E: 'static + Component, + F: FnOnce(&mut WindowContext) -> E + 'static, { - type ElementState = AnyElement; + type ElementState = AnyElement; fn element_id(&self) -> Option { None @@ -357,45 +345,41 @@ where fn layout( &mut self, - view_state: &mut V, _: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { let render = self.take().unwrap(); - let mut rendered_element = (render)(view_state, cx).render(); - let layout_id = rendered_element.layout(view_state, cx); + let mut rendered_element = (render)(cx).render(); + let layout_id = rendered_element.layout(cx); (layout_id, rendered_element) } fn paint( &mut self, _bounds: Bounds, - view_state: &mut V, rendered_element: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - rendered_element.paint(view_state, cx) + rendered_element.paint(cx) } } -impl Component for Option +impl Component for Option where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, + E: 'static + Component, + F: FnOnce(&mut WindowContext) -> E + 'static, { - fn render(self) -> AnyElement { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Component for F +impl Component for F where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, + E: 'static + Component, + F: FnOnce(&mut WindowContext) -> E + 'static, { - fn render(self) -> AnyElement { + fn render(self) -> AnyElement { AnyElement::new(Some(self)) } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index a37e3dee2a..6587f22552 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,9 +1,10 @@ use crate::{ point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, - BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent, - FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent, - SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, + BorrowWindow, Bounds, CallbackHandle, ClickEvent, Component, ConstructorHandle, DispatchPhase, + Element, ElementId, FocusEvent, FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, + Render, ScrollWheelEvent, SharedString, Size, Style, StyleRefinement, Styled, Task, View, + Visibility, WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -12,7 +13,6 @@ use std::{ any::{Any, TypeId}, cell::RefCell, fmt::Debug, - marker::PhantomData, mem, rc::Rc, time::Duration, @@ -28,30 +28,24 @@ pub struct GroupStyle { pub style: StyleRefinement, } -pub trait InteractiveComponent: Sized + Element { - fn interactivity(&mut self) -> &mut Interactivity; +pub trait InteractiveComponent: Sized + Element { + fn interactivity(&mut self) -> &mut Interactivity; fn group(mut self, group: impl Into) -> Self { self.interactivity().group = Some(group.into()); self } - fn id(mut self, id: impl Into) -> Stateful { + fn id(mut self, id: impl Into) -> Stateful { self.interactivity().element_id = Some(id.into()); - Stateful { - element: self, - view_type: PhantomData, - } + Stateful { element: self } } - fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable { + fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable { self.interactivity().focusable = true; self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); - Focusable { - element: self, - view_type: PhantomData, - } + Focusable { element: self } } fn key_context(mut self, key_context: C) -> Self @@ -85,29 +79,28 @@ pub trait InteractiveComponent: Sized + Element { fn on_mouse_down( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + handler: impl Into>, ) -> Self { + let handler = handler.into(); self.interactivity().mouse_down_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && event.button == button && bounds.contains_point(&event.position) { - handler(view, event, cx) + (handler.callback)(event, cx) } }, )); self } - fn on_any_mouse_down( - mut self, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self { + fn on_any_mouse_down(mut self, handler: impl Into>) -> Self { + let handler = handler.into(); self.interactivity().mouse_down_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx) + (handler.callback)(event, cx) } }, )); @@ -117,43 +110,40 @@ pub trait InteractiveComponent: Sized + Element { fn on_mouse_up( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + handler: impl Into>, ) -> Self { - self.interactivity().mouse_up_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + let handler = handler.into(); + self.interactivity() + .mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && event.button == button && bounds.contains_point(&event.position) { - handler(view, event, cx) + (handler.callback)(event, cx) } - }, - )); + })); self } - fn on_any_mouse_up( - mut self, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, - ) -> Self { - self.interactivity().mouse_up_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + fn on_any_mouse_up(mut self, handler: impl Into>) -> Self { + let handler = handler.into(); + self.interactivity() + .mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx) + (handler.callback)(event, cx) } - }, - )); + })); self } - fn on_mouse_down_out( - mut self, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self { + fn on_mouse_down_out(mut self, handler: impl Into>) -> Self { + let handler = handler.into(); self.interactivity().mouse_down_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { - handler(view, event, cx) + (handler.callback)(event, cx) } }, )); @@ -163,60 +153,55 @@ pub trait InteractiveComponent: Sized + Element { fn on_mouse_up_out( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + handler: impl Into>, ) -> Self { - self.interactivity().mouse_up_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + let handler = handler.into(); + self.interactivity() + .mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Capture && event.button == button && !bounds.contains_point(&event.position) { - handler(view, event, cx); + (handler.callback)(event, cx); } - }, - )); + })); self } - fn on_mouse_move( - mut self, - handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + 'static, - ) -> Self { + fn on_mouse_move(mut self, handler: impl Into>) -> Self { + let handler = handler.into(); self.interactivity().mouse_move_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx); + (handler.callback)(event, cx); } }, )); self } - fn on_scroll_wheel( - mut self, - handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + 'static, - ) -> Self { + fn on_scroll_wheel(mut self, handler: impl Into>) -> Self { + let handler = handler.into(); self.interactivity().scroll_wheel_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx); + (handler.callback)(event, cx); } }, )); self } - /// Capture the given action, fires during the capture phase - fn capture_action( - mut self, - listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, - ) -> Self { + /// Capture the given action, before normal action dispatch can fire + fn capture_action(mut self, listener: impl Into>) -> Self { + let listener = listener.into(); self.interactivity().action_listeners.push(( TypeId::of::(), - Box::new(move |view, action, phase, cx| { + Box::new(move |action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Capture { - listener(view, action, cx) + (listener.callback)(action, cx) } }), )); @@ -224,10 +209,8 @@ pub trait InteractiveComponent: Sized + Element { } /// Add a listener for the given action, fires during the bubble event phase - fn on_action( - mut self, - listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, - ) -> Self { + fn on_action(mut self, listener: impl Into> + 'static) -> Self { + let handle = listener.into(); // NOTE: this debug assert has the side-effect of working around // a bug where a crate consisting only of action definitions does // not register the actions in debug builds: @@ -244,36 +227,60 @@ pub trait InteractiveComponent: Sized + Element { // ); self.interactivity().action_listeners.push(( TypeId::of::(), - Box::new(move |view, action, phase, cx| { + Box::new(move |action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Bubble { - listener(view, action, cx) + (handle.callback)(action, cx) } }), )); self } - fn on_key_down( - mut self, - listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + 'static, - ) -> Self { + fn on_key_down(mut self, listener: impl Into>) -> Self { + let listener = listener.into(); self.interactivity() .key_down_listeners - .push(Box::new(move |view, event, phase, cx| { - listener(view, event, phase, cx) + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener.callback)(event, cx) + } })); self } - fn on_key_up( - mut self, - listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + 'static, - ) -> Self { + fn capture_key_down(mut self, listener: impl Into>) -> Self { + let listener = listener.into(); + self.interactivity() + .key_down_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Capture { + (listener.callback)(event, cx) + } + })); + self + } + + fn on_key_up(mut self, listener: impl Into>) -> Self { + let listener = listener.into(); self.interactivity() .key_up_listeners - .push(Box::new(move |view, event, phase, cx| { - listener(view, event, phase, cx) + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener.callback)(event, cx) + } + })); + self + } + + fn capture_key_up(mut self, listener: impl Into>) -> Self { + let listener = listener.into(); + self.interactivity() + .key_up_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Capture { + (listener.callback)(event, cx) + } })); self } @@ -300,27 +307,22 @@ pub trait InteractiveComponent: Sized + Element { self } - fn on_drop( - mut self, - listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, - ) -> Self { + fn on_drop(mut self, listener: impl Into>>) -> Self { + let listener = listener.into(); self.interactivity().drop_listeners.push(( TypeId::of::(), - Box::new(move |view, dragged_view, cx| { - listener(view, dragged_view.downcast().unwrap(), cx); + Box::new(move |dragged_view, cx| { + (listener.callback)(&dragged_view.downcast().unwrap(), cx); }), )); self } } -pub trait StatefulInteractiveComponent>: InteractiveComponent { - fn focusable(mut self) -> Focusable { +pub trait StatefulInteractiveComponent: InteractiveComponent { + fn focusable(mut self) -> Focusable { self.interactivity().focusable = true; - Focusable { - element: self, - view_type: PhantomData, - } + Focusable { element: self } } fn overflow_scroll(mut self) -> Self { @@ -362,70 +364,64 @@ pub trait StatefulInteractiveComponent>: InteractiveCo self } - fn on_click( - mut self, - listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_click(mut self, listener: impl Into>) -> Self where Self: Sized, { + let listener = listener.into(); self.interactivity() .click_listeners - .push(Box::new(move |view, event, cx| listener(view, event, cx))); + .push(Box::new(move |event, cx| (listener.callback)(event, cx))); self } - fn on_drag( - mut self, - listener: impl Fn(&mut V, &mut ViewContext) -> View + 'static, - ) -> Self + fn on_drag(mut self, listener: impl Into>>) -> Self where Self: Sized, W: 'static + Render, { + let listener = listener.into(); debug_assert!( self.interactivity().drag_listener.is_none(), "calling on_drag more than once on the same element is not supported" ); - self.interactivity().drag_listener = - Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag { - view: listener(view_state, cx).into(), - cursor_offset, - })); + self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag { + view: (listener.callback)(cx).into(), + cursor_offset, + })); self } - fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext)) -> Self + fn on_hover(mut self, listener: impl Into>) -> Self where Self: Sized, { + let listener = listener.into(); debug_assert!( self.interactivity().hover_listener.is_none(), "calling on_hover more than once on the same element is not supported" ); - self.interactivity().hover_listener = Some(Box::new(listener)); + self.interactivity().hover_listener = Some(listener); self } - fn tooltip( - mut self, - build_tooltip: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, - ) -> Self + fn tooltip(mut self, build_tooltip: impl Into>) -> Self where Self: Sized, { + let build_tooltip = build_tooltip.into(); debug_assert!( self.interactivity().tooltip_builder.is_none(), "calling tooltip more than once on the same element is not supported" ); self.interactivity().tooltip_builder = - Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx))); + Some(Rc::new(move |cx| (build_tooltip.callback)(cx))); self } } -pub trait FocusableComponent: InteractiveComponent { +pub trait FocusableComponent: InteractiveComponent { fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -442,49 +438,44 @@ pub trait FocusableComponent: InteractiveComponent { self } - fn on_focus( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_focus(mut self, listener: impl Into>) -> Self where Self: Sized, { - self.interactivity().focus_listeners.push(Box::new( - move |view, focus_handle, event, cx| { + let listener = listener.into(); + self.interactivity() + .focus_listeners + .push(Box::new(move |focus_handle, event, cx| { if event.focused.as_ref() == Some(focus_handle) { - listener(view, event, cx) + (listener.callback)(event, cx) } - }, - )); + })); self } - fn on_blur( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_blur(mut self, listener: impl Into>) -> Self where Self: Sized, { - self.interactivity().focus_listeners.push(Box::new( - move |view, focus_handle, event, cx| { + let listener = listener.into(); + self.interactivity() + .focus_listeners + .push(Box::new(move |focus_handle, event, cx| { if event.blurred.as_ref() == Some(focus_handle) { - listener(view, event, cx) + (listener.callback)(event, cx) } - }, - )); + })); self } - fn on_focus_in( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_focus_in(mut self, listener: impl Into>) -> Self where Self: Sized, { - self.interactivity().focus_listeners.push(Box::new( - move |view, focus_handle, event, cx| { + let listener = listener.into(); + self.interactivity() + .focus_listeners + .push(Box::new(move |focus_handle, event, cx| { let descendant_blurred = event .blurred .as_ref() @@ -495,22 +486,20 @@ pub trait FocusableComponent: InteractiveComponent { .map_or(false, |focused| focus_handle.contains(focused, cx)); if !descendant_blurred && descendant_focused { - listener(view, event, cx) + (listener.callback)(event, cx) } - }, - )); + })); self } - fn on_focus_out( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_focus_out(mut self, listener: impl Into>) -> Self where Self: Sized, { - self.interactivity().focus_listeners.push(Box::new( - move |view, focus_handle, event, cx| { + let listener = listener.into(); + self.interactivity() + .focus_listeners + .push(Box::new(move |focus_handle, event, cx| { let descendant_blurred = event .blurred .as_ref() @@ -520,86 +509,73 @@ pub trait FocusableComponent: InteractiveComponent { .as_ref() .map_or(false, |focused| focus_handle.contains(focused, cx)); if descendant_blurred && !descendant_focused { - listener(view, event, cx) + (listener.callback)(event, cx) } - }, - )); + })); self } } -pub type FocusListeners = SmallVec<[FocusListener; 2]>; +pub type FocusListeners = SmallVec<[FocusListener; 2]>; -pub type FocusListener = - Box) + 'static>; +pub type FocusListener = Box; -pub type MouseDownListener = Box< - dyn Fn(&mut V, &MouseDownEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; -pub type MouseUpListener = Box< - dyn Fn(&mut V, &MouseUpEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; +pub type MouseDownListener = + Box, DispatchPhase, &mut WindowContext) + 'static>; +pub type MouseUpListener = + Box, DispatchPhase, &mut WindowContext) + 'static>; -pub type MouseMoveListener = Box< - dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; +pub type MouseMoveListener = + Box, DispatchPhase, &mut WindowContext) + 'static>; -pub type ScrollWheelListener = Box< - dyn Fn(&mut V, &ScrollWheelEvent, &Bounds, DispatchPhase, &mut ViewContext) - + 'static, ->; +pub type ScrollWheelListener = + Box, DispatchPhase, &mut WindowContext) + 'static>; -pub type ClickListener = Box) + 'static>; +pub type ClickListener = Box; -pub type DragListener = - Box, &mut ViewContext) -> AnyDrag + 'static>; +pub type DragListener = Box, &mut WindowContext) -> AnyDrag + 'static>; -type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; +type DropListener = dyn Fn(AnyView, &mut WindowContext) + 'static; -pub type HoverListener = Box) + 'static>; +pub type TooltipBuilder = Rc AnyView + 'static>; -pub type TooltipBuilder = Rc) -> AnyView + 'static>; +pub type KeyDownListener = Box; -pub type KeyDownListener = - Box) + 'static>; +pub type KeyUpListener = Box; -pub type KeyUpListener = - Box) + 'static>; +pub type ActionListener = Box; -pub type ActionListener = - Box) + 'static>; - -pub fn div() -> Div { +pub fn div() -> Div { Div { interactivity: Interactivity::default(), children: SmallVec::default(), } } -pub struct Div { - interactivity: Interactivity, - children: SmallVec<[AnyElement; 2]>, +pub struct Div { + interactivity: Interactivity, + children: SmallVec<[AnyElement; 2]>, } -impl Styled for Div { +impl Styled for Div { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveComponent for Div { - fn interactivity(&mut self) -> &mut Interactivity { +impl InteractiveComponent for Div { + fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } -impl ParentComponent for Div { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { +impl ParentComponent for Div { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } } -impl Element for Div { +impl Element for Div { type ElementState = DivState; fn element_id(&self) -> Option { @@ -608,9 +584,8 @@ impl Element for Div { fn layout( &mut self, - view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { let mut child_layout_ids = SmallVec::new(); let mut interactivity = mem::take(&mut self.interactivity); @@ -622,7 +597,7 @@ impl Element for Div { child_layout_ids = self .children .iter_mut() - .map(|child| child.layout(view_state, cx)) + .map(|child| child.layout(cx)) .collect::>(); cx.request_layout(&style, child_layout_ids.iter().copied()) }) @@ -641,9 +616,8 @@ impl Element for Div { fn paint( &mut self, bounds: Bounds, - view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); @@ -680,7 +654,7 @@ impl Element for Div { cx.with_content_mask(style.overflow_mask(bounds), |cx| { cx.with_element_offset(scroll_offset, |cx| { for child in &mut self.children { - child.paint(view_state, cx); + child.paint(cx); } }) }) @@ -693,8 +667,8 @@ impl Element for Div { } } -impl Component for Div { - fn render(self) -> AnyElement { +impl Component for Div { + fn render(self) -> AnyElement { AnyElement::new(self) } } @@ -710,12 +684,12 @@ impl DivState { } } -pub struct Interactivity { +pub struct Interactivity { pub element_id: Option, pub key_context: KeyContext, pub focusable: bool, pub tracked_focus_handle: Option, - pub focus_listeners: FocusListeners, + pub focus_listeners: FocusListeners, pub group: Option, pub base_style: StyleRefinement, pub focus_style: StyleRefinement, @@ -726,29 +700,26 @@ pub struct Interactivity { pub group_active_style: Option, pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, - pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, - pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, - pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, - pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, - pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, - pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, - pub drop_listeners: SmallVec<[(TypeId, Box>); 2]>, - pub click_listeners: SmallVec<[ClickListener; 2]>, - pub drag_listener: Option>, - pub hover_listener: Option>, - pub tooltip_builder: Option>, + pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, + pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, + pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, + pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, + pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, + pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, + pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, + pub drop_listeners: SmallVec<[(TypeId, Box); 2]>, + pub click_listeners: SmallVec<[ClickListener; 2]>, + pub drag_listener: Option, + pub hover_listener: Option>, + pub tooltip_builder: Option, } -impl Interactivity -where - V: 'static, -{ +impl Interactivity { pub fn layout( &mut self, element_state: Option, - cx: &mut ViewContext, - f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, + cx: &mut WindowContext, + f: impl FnOnce(Style, &mut WindowContext) -> LayoutId, ) -> (LayoutId, InteractiveElementState) { let mut element_state = element_state.unwrap_or_default(); @@ -774,8 +745,8 @@ where bounds: Bounds, content_size: Size, element_state: &mut InteractiveElementState, - cx: &mut ViewContext, - f: impl FnOnce(Style, Point, &mut ViewContext), + cx: &mut WindowContext, + f: impl FnOnce(Style, Point, &mut WindowContext), ) { let style = self.compute_style(Some(bounds), element_state, cx); @@ -787,26 +758,26 @@ where } for listener in self.mouse_down_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } for listener in self.mouse_up_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } for listener in self.mouse_move_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } for listener in self.scroll_wheel_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } @@ -817,7 +788,7 @@ where if let Some(group_bounds) = hover_group_bounds { let hovered = group_bounds.contains_point(&cx.mouse_position()); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { if group_bounds.contains_point(&event.position) != hovered { cx.notify(); @@ -830,7 +801,7 @@ where || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty()) { let hovered = bounds.contains_point(&cx.mouse_position()); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { if bounds.contains_point(&event.position) != hovered { cx.notify(); @@ -841,7 +812,7 @@ where if cx.active_drag.is_some() { let drop_listeners = mem::take(&mut self.drop_listeners); - cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { if let Some(drag_state_type) = cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) @@ -852,7 +823,7 @@ where .active_drag .take() .expect("checked for type drag state type above"); - listener(view, drag.view.clone(), cx); + listener(drag.view.clone(), cx); cx.notify(); cx.stop_propagation(); } @@ -872,7 +843,7 @@ where if let Some(drag_listener) = drag_listener { let active_state = element_state.clicked_state.clone(); - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if cx.active_drag.is_some() { if phase == DispatchPhase::Capture { cx.notify(); @@ -883,7 +854,7 @@ where { *active_state.borrow_mut() = ElementClickedState::default(); let cursor_offset = event.position - bounds.origin; - let drag = drag_listener(view_state, cursor_offset, cx); + let drag = drag_listener(cursor_offset, cx); cx.active_drag = Some(drag); cx.notify(); cx.stop_propagation(); @@ -891,21 +862,21 @@ where }); } - cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { let mouse_click = ClickEvent { down: mouse_down.clone(), up: event.clone(), }; for listener in &click_listeners { - listener(view_state, &mouse_click, cx); + listener(&mouse_click, cx); } } *pending_mouse_down.borrow_mut() = None; cx.notify(); }); } else { - cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { *pending_mouse_down.borrow_mut() = Some(event.clone()); cx.notify(); @@ -918,7 +889,7 @@ where let was_hovered = element_state.hover_state.clone(); let has_mouse_down = element_state.pending_mouse_down.clone(); - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } @@ -930,7 +901,7 @@ where *was_hovered = is_hovered; drop(was_hovered); - hover_listener(view_state, is_hovered, cx); + (hover_listener.callback)(&is_hovered, cx); } }); } @@ -939,7 +910,7 @@ where let active_tooltip = element_state.active_tooltip.clone(); let pending_mouse_down = element_state.pending_mouse_down.clone(); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } @@ -956,12 +927,12 @@ where let active_tooltip = active_tooltip.clone(); let tooltip_builder = tooltip_builder.clone(); - move |view, mut cx| async move { + move |mut cx| async move { cx.background_executor().timer(TOOLTIP_DELAY).await; - view.update(&mut cx, move |view_state, cx| { + cx.update(|_, cx| { active_tooltip.borrow_mut().replace(ActiveTooltip { tooltip: Some(AnyTooltip { - view: tooltip_builder(view_state, cx), + view: tooltip_builder(cx), cursor_offset: cx.mouse_position(), }), _task: None, @@ -979,7 +950,7 @@ where }); let active_tooltip = element_state.active_tooltip.clone(); - cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| { + cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { active_tooltip.borrow_mut().take(); }); @@ -992,7 +963,7 @@ where let active_state = element_state.clicked_state.clone(); if !active_state.borrow().is_clicked() { - cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| { + cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Capture { *active_state.borrow_mut() = ElementClickedState::default(); cx.notify(); @@ -1003,7 +974,7 @@ where .group_active_style .as_ref() .and_then(|group_active| GroupBounds::get(&group_active.group, cx)); - cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| { + cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble { let group = active_group_bounds .map_or(false, |bounds| bounds.contains_point(&down.position)); @@ -1025,7 +996,7 @@ where let line_height = cx.line_height(); let scroll_max = (content_size - bounds.size).max(&Size::default()); - cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| { + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { let mut scroll_offset = scroll_offset.borrow_mut(); let old_scroll_offset = *scroll_offset; @@ -1063,14 +1034,14 @@ where element_state.focus_handle.clone(), |_, cx| { for listener in self.key_down_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { - listener(state, event, phase, cx); + cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { + listener(event, phase, cx); }) } for listener in self.key_up_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { - listener(state, event, phase, cx); + cx.on_key_event(move |event: &KeyUpEvent, phase, cx| { + listener(event, phase, cx); }) } @@ -1081,9 +1052,7 @@ where if let Some(focus_handle) = element_state.focus_handle.as_ref() { for listener in self.focus_listeners.drain(..) { let focus_handle = focus_handle.clone(); - cx.on_focus_changed(move |view, event, cx| { - listener(view, &focus_handle, event, cx) - }); + cx.on_focus_changed(move |event, cx| listener(&focus_handle, event, cx)); } } @@ -1100,7 +1069,7 @@ where &self, bounds: Option>, element_state: &mut InteractiveElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Style { let mut style = Style::default(); style.refine(&self.base_style); @@ -1171,7 +1140,7 @@ where } } -impl Default for Interactivity { +impl Default for Interactivity { fn default() -> Self { Self { element_id: None, @@ -1259,31 +1228,25 @@ impl GroupBounds { } } -pub struct Focusable { +pub struct Focusable { element: E, - view_type: PhantomData, } -impl> FocusableComponent for Focusable {} +impl FocusableComponent for Focusable {} -impl InteractiveComponent for Focusable +impl InteractiveComponent for Focusable where - V: 'static, - E: InteractiveComponent, + E: InteractiveComponent, { - fn interactivity(&mut self) -> &mut Interactivity { + fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl> StatefulInteractiveComponent - for Focusable -{ -} +impl StatefulInteractiveComponent for Focusable {} -impl Styled for Focusable +impl Styled for Focusable where - V: 'static, E: Styled, { fn style(&mut self) -> &mut StyleRefinement { @@ -1291,10 +1254,9 @@ where } } -impl Element for Focusable +impl Element for Focusable where - V: 'static, - E: Element, + E: Element, { type ElementState = E::ElementState; @@ -1304,52 +1266,46 @@ where fn layout( &mut self, - view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { - self.element.layout(view_state, element_state, cx) + self.element.layout(element_state, cx) } fn paint( &mut self, bounds: Bounds, - view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - self.element.paint(bounds, view_state, element_state, cx); + self.element.paint(bounds, element_state, cx); } } -impl Component for Focusable +impl Component for Focusable where - V: 'static, - E: 'static + Element, + E: 'static + Element, { - fn render(self) -> AnyElement { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl ParentComponent for Focusable +impl ParentComponent for Focusable where - V: 'static, - E: ParentComponent, + E: ParentComponent, { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { self.element.children_mut() } } -pub struct Stateful { +pub struct Stateful { element: E, - view_type: PhantomData, } -impl Styled for Stateful +impl Styled for Stateful where - V: 'static, E: Styled, { fn style(&mut self) -> &mut StyleRefinement { @@ -1357,30 +1313,27 @@ where } } -impl StatefulInteractiveComponent for Stateful +impl StatefulInteractiveComponent for Stateful where - V: 'static, - E: Element, - Self: InteractiveComponent, + E: Element, + Self: InteractiveComponent, { } -impl InteractiveComponent for Stateful +impl InteractiveComponent for Stateful where - V: 'static, - E: InteractiveComponent, + E: InteractiveComponent, { - fn interactivity(&mut self) -> &mut Interactivity { + fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl> FocusableComponent for Stateful {} +impl FocusableComponent for Stateful {} -impl Element for Stateful +impl Element for Stateful where - V: 'static, - E: Element, + E: Element, { type ElementState = E::ElementState; @@ -1390,40 +1343,36 @@ where fn layout( &mut self, - view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { - self.element.layout(view_state, element_state, cx) + self.element.layout(element_state, cx) } fn paint( &mut self, bounds: Bounds, - view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - self.element.paint(bounds, view_state, element_state, cx) + self.element.paint(bounds, element_state, cx) } } -impl Component for Stateful +impl Component for Stateful where - V: 'static, - E: 'static + Element, + E: 'static + Element, { - fn render(self) -> AnyElement { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl ParentComponent for Stateful +impl ParentComponent for Stateful where - V: 'static, - E: ParentComponent, + E: ParentComponent, { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { self.element.children_mut() } } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 5376c40012..023811dcd2 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,18 +1,17 @@ use crate::{ - AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent, - InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, - Styled, ViewContext, + AnyElement, Bounds, Component, Element, InteractiveComponent, InteractiveElementState, + Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, }; use futures::FutureExt; use util::ResultExt; -pub struct Img { - interactivity: Interactivity, +pub struct Img { + interactivity: Interactivity, uri: Option, grayscale: bool, } -pub fn img() -> Img { +pub fn img() -> Img { Img { interactivity: Interactivity::default(), uri: None, @@ -20,10 +19,7 @@ pub fn img() -> Img { } } -impl Img -where - V: 'static, -{ +impl Img { pub fn uri(mut self, uri: impl Into) -> Self { self.uri = Some(uri.into()); self @@ -35,13 +31,13 @@ where } } -impl Component for Img { - fn render(self) -> AnyElement { +impl Component for Img { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Img { +impl Element for Img { type ElementState = InteractiveElementState; fn element_id(&self) -> Option { @@ -50,9 +46,8 @@ impl Element for Img { fn layout( &mut self, - _view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) @@ -62,9 +57,8 @@ impl Element for Img { fn paint( &mut self, bounds: Bounds, - _view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { self.interactivity.paint( bounds, @@ -89,7 +83,7 @@ impl Element for Img { .log_err() }); } else { - cx.spawn(|_, mut cx| async move { + cx.spawn(|mut cx| async move { if image_future.await.ok().is_some() { cx.on_next_frame(|cx| cx.notify()); } @@ -102,14 +96,14 @@ impl Element for Img { } } -impl Styled for Img { +impl Styled for Img { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveComponent for Img { - fn interactivity(&mut self) -> &mut Interactivity { +impl InteractiveComponent for Img { + fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 14a8048d39..19dab158cc 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -3,15 +3,15 @@ use taffy::style::{Display, Position}; use crate::{ point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, - Point, Size, Style, + Point, Size, Style, WindowContext, }; pub struct OverlayState { child_layout_ids: SmallVec<[LayoutId; 4]>, } -pub struct Overlay { - children: SmallVec<[AnyElement; 2]>, +pub struct Overlay { + children: SmallVec<[AnyElement; 2]>, anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, // todo!(); @@ -21,7 +21,7 @@ pub struct Overlay { /// overlay gives you a floating element that will avoid overflowing the window bounds. /// Its children should have no margin to avoid measurement issues. -pub fn overlay() -> Overlay { +pub fn overlay() -> Overlay { Overlay { children: SmallVec::new(), anchor_corner: AnchorCorner::TopLeft, @@ -30,7 +30,7 @@ pub fn overlay() -> Overlay { } } -impl Overlay { +impl Overlay { /// Sets which corner of the overlay should be anchored to the current position. pub fn anchor(mut self, anchor: AnchorCorner) -> Self { self.anchor_corner = anchor; @@ -51,19 +51,19 @@ impl Overlay { } } -impl ParentComponent for Overlay { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { +impl ParentComponent for Overlay { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } } -impl Component for Overlay { - fn render(self) -> AnyElement { +impl Component for Overlay { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Overlay { +impl Element for Overlay { type ElementState = OverlayState; fn element_id(&self) -> Option { @@ -72,14 +72,13 @@ impl Element for Overlay { fn layout( &mut self, - view_state: &mut V, _: Option, - cx: &mut crate::ViewContext, + cx: &mut WindowContext, ) -> (crate::LayoutId, Self::ElementState) { let child_layout_ids = self .children .iter_mut() - .map(|child| child.layout(view_state, cx)) + .map(|child| child.layout(cx)) .collect::>(); let mut overlay_style = Style::default(); @@ -94,9 +93,8 @@ impl Element for Overlay { fn paint( &mut self, bounds: crate::Bounds, - view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut crate::ViewContext, + cx: &mut WindowContext, ) { if element_state.child_layout_ids.is_empty() { return; @@ -157,7 +155,7 @@ impl Element for Overlay { cx.with_element_offset(desired.origin - bounds.origin, |cx| { for child in &mut self.children { - child.paint(view_state, cx); + child.paint(cx); } }) } diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index c1c7691fbf..2a0df36c94 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,36 +1,36 @@ use crate::{ AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, - Styled, ViewContext, + Styled, WindowContext, }; use util::ResultExt; -pub struct Svg { - interactivity: Interactivity, +pub struct Svg { + interactivity: Interactivity, path: Option, } -pub fn svg() -> Svg { +pub fn svg() -> Svg { Svg { interactivity: Interactivity::default(), path: None, } } -impl Svg { +impl Svg { pub fn path(mut self, path: impl Into) -> Self { self.path = Some(path.into()); self } } -impl Component for Svg { - fn render(self) -> AnyElement { +impl Component for Svg { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Svg { +impl Element for Svg { type ElementState = InteractiveElementState; fn element_id(&self) -> Option { @@ -39,9 +39,8 @@ impl Element for Svg { fn layout( &mut self, - _view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) @@ -51,9 +50,8 @@ impl Element for Svg { fn paint( &mut self, bounds: Bounds, - _view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) where Self: Sized, { @@ -66,14 +64,14 @@ impl Element for Svg { } } -impl Styled for Svg { +impl Styled for Svg { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveComponent for Svg { - fn interactivity(&mut self) -> &mut Interactivity { +impl InteractiveComponent for Svg { + fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index e3c63523e1..99226baaa7 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,6 +1,6 @@ use crate::{ - AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels, - SharedString, Size, TextRun, ViewContext, WrappedLine, + AnyElement, Bounds, Component, Element, ElementId, LayoutId, Pixels, SharedString, Size, + TextRun, WindowContext, WrappedLine, }; use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; @@ -26,13 +26,13 @@ impl Text { } } -impl Component for Text { - fn render(self) -> AnyElement { +impl Component for Text { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Text { +impl Element for Text { type ElementState = TextState; fn element_id(&self) -> Option { @@ -41,9 +41,8 @@ impl Element for Text { fn layout( &mut self, - _view: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { let element_state = element_state.unwrap_or_default(); let text_system = cx.text_system().clone(); @@ -120,9 +119,8 @@ impl Element for Text { fn paint( &mut self, bounds: Bounds, - _: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let element_state = element_state.lock(); let element_state = element_state @@ -165,7 +163,7 @@ struct InteractiveTextState { clicked_range_ixs: Rc>>, } -impl Element for InteractiveText { +impl Element for InteractiveText { type ElementState = InteractiveTextState; fn element_id(&self) -> Option { @@ -174,23 +172,22 @@ impl Element for InteractiveText { fn layout( &mut self, - view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { if let Some(InteractiveTextState { text_state, clicked_range_ixs, }) = element_state { - let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx); + let (layout_id, text_state) = self.text.layout(Some(text_state), cx); let element_state = InteractiveTextState { text_state, clicked_range_ixs, }; (layout_id, element_state) } else { - let (layout_id, text_state) = self.text.layout(view_state, None, cx); + let (layout_id, text_state) = self.text.layout(None, cx); let element_state = InteractiveTextState { text_state, clicked_range_ixs: Rc::default(), @@ -202,17 +199,15 @@ impl Element for InteractiveText { fn paint( &mut self, bounds: Bounds, - view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - self.text - .paint(bounds, view_state, &mut element_state.text_state, cx) + self.text.paint(bounds, &mut element_state.text_state, cx) } } -impl Component for SharedString { - fn render(self) -> AnyElement { +impl Component for SharedString { + fn render(self) -> AnyElement { Text { text: self, runs: None, @@ -221,8 +216,8 @@ impl Component for SharedString { } } -impl Component for &'static str { - fn render(self) -> AnyElement { +impl Component for &'static str { + fn render(self) -> AnyElement { Text { text: self.into(), runs: None, @@ -233,8 +228,8 @@ impl Component for &'static str { // TODO: Figure out how to pass `String` to `child` without this. // This impl doesn't exist in the `gpui2` crate. -impl Component for String { - fn render(self) -> AnyElement { +impl Component for String { + fn render(self) -> AnyElement { Text { text: self.into(), runs: None, diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 773f9ec8aa..48d666383f 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,7 +1,7 @@ use crate::{ - point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, - ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, - Point, Size, StyleRefinement, Styled, ViewContext, + point, px, size, AnyElement, AvailableSpace, Bounds, Component, Element, ElementId, + InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Size, + StyleRefinement, Styled, WindowContext, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc}; @@ -10,15 +10,14 @@ use taffy::style::Overflow; /// uniform_list provides lazy rendering for a set of items that are of uniform height. /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, /// uniform_list will only render the visibile subset of items. -pub fn uniform_list( +pub fn uniform_list( id: I, item_count: usize, - f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, -) -> UniformList + f: impl 'static + Fn(Range, &mut WindowContext) -> Vec, +) -> UniformList where I: Into, - V: 'static, - C: Component, + C: Component, { let id = id.into(); let mut style = StyleRefinement::default(); @@ -29,8 +28,8 @@ where style, item_count, item_to_measure_index: 0, - render_items: Box::new(move |view, visible_range, cx| { - f(view, visible_range, cx) + render_items: Box::new(move |visible_range, cx| { + f(visible_range, cx) .into_iter() .map(|component| component.render()) .collect() @@ -43,19 +42,14 @@ where } } -pub struct UniformList { +pub struct UniformList { id: ElementId, style: StyleRefinement, item_count: usize, item_to_measure_index: usize, - render_items: Box< - dyn for<'a> Fn( - &'a mut V, - Range, - &'a mut ViewContext, - ) -> SmallVec<[AnyElement; 64]>, - >, - interactivity: Interactivity, + render_items: + Box Fn(Range, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>, + interactivity: Interactivity, scroll_handle: Option, } @@ -89,7 +83,7 @@ impl UniformListScrollHandle { } } -impl Styled for UniformList { +impl Styled for UniformList { fn style(&mut self) -> &mut StyleRefinement { &mut self.style } @@ -101,7 +95,7 @@ pub struct UniformListState { item_size: Size, } -impl Element for UniformList { +impl Element for UniformList { type ElementState = UniformListState; fn element_id(&self) -> Option { @@ -110,16 +104,15 @@ impl Element for UniformList { fn layout( &mut self, - view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { let max_items = self.item_count; let rem_size = cx.rem_size(); let item_size = element_state .as_ref() .map(|s| s.item_size) - .unwrap_or_else(|| self.measure_item(view_state, None, cx)); + .unwrap_or_else(|| self.measure_item(None, cx)); let (layout_id, interactive) = self.interactivity @@ -161,9 +154,8 @@ impl Element for UniformList { fn paint( &mut self, bounds: Bounds, - view_state: &mut V, element_state: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let style = self.interactivity @@ -209,9 +201,8 @@ impl Element for UniformList { style.paint(bounds, cx); if self.item_count > 0 { - let item_height = self - .measure_item(view_state, Some(padded_bounds.size.width), cx) - .height; + let item_height = + self.measure_item(Some(padded_bounds.size.width), cx).height; if let Some(scroll_handle) = self.scroll_handle.clone() { scroll_handle.0.borrow_mut().replace(ScrollHandleState { item_height, @@ -233,7 +224,7 @@ impl Element for UniformList { self.item_count, ); - let mut items = (self.render_items)(view_state, visible_range.clone(), cx); + let mut items = (self.render_items)(visible_range.clone(), cx); cx.with_z_index(1, |cx| { for (item, ix) in items.iter_mut().zip(visible_range) { let item_origin = padded_bounds.origin @@ -242,7 +233,7 @@ impl Element for UniformList { AvailableSpace::Definite(padded_bounds.size.width), AvailableSpace::Definite(item_height), ); - item.draw(item_origin, available_space, view_state, cx); + item.draw(item_origin, available_space, cx); } }); } @@ -253,24 +244,19 @@ impl Element for UniformList { } } -impl UniformList { +impl UniformList { pub fn with_width_from_item(mut self, item_index: Option) -> Self { self.item_to_measure_index = item_index.unwrap_or(0); self } - fn measure_item( - &self, - view_state: &mut V, - list_width: Option, - cx: &mut ViewContext, - ) -> Size { + fn measure_item(&self, list_width: Option, cx: &mut WindowContext) -> Size { if self.item_count == 0 { return Size::default(); } let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); - let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx); + let mut items = (self.render_items)(item_ix..item_ix + 1, cx); let mut item_to_measure = items.pop().unwrap(); let available_space = size( list_width.map_or(AvailableSpace::MinContent, |width| { @@ -278,7 +264,7 @@ impl UniformList { }), AvailableSpace::MinContent, ); - item_to_measure.measure(available_space, view_state, cx) + item_to_measure.measure(available_space, cx) } pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { @@ -287,14 +273,14 @@ impl UniformList { } } -impl InteractiveComponent for UniformList { - fn interactivity(&mut self) -> &mut crate::Interactivity { +impl InteractiveComponent for UniformList { + fn interactivity(&mut self) -> &mut crate::Interactivity { &mut self.interactivity } } -impl Component for UniformList { - fn render(self) -> AnyElement { +impl Component for UniformList { + fn render(self) -> AnyElement { AnyElement::new(self) } } diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index a24509386b..39b148b15b 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -196,6 +196,30 @@ where } } +pub struct CallbackHandle { + callback: Box, +} + +impl From for CallbackHandle { + fn from(value: F) -> Self { + CallbackHandle { + callback: Box::new(value), + } + } +} + +pub struct ConstructorHandle { + callback: Box R + 'static>, +} + +impl R + 'static> From for ConstructorHandle { + fn from(value: F) -> Self { + ConstructorHandle { + callback: Box::new(value), + } + } +} + pub trait Flatten { fn flatten(self) -> Result; } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 80a89ef625..2604d24f89 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,9 +1,8 @@ use crate::{ - div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, - ViewContext, + div, point, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, }; use smallvec::SmallVec; -use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; +use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct KeyDownEvent { @@ -60,32 +59,6 @@ pub struct ClickEvent { pub up: MouseUpEvent, } -pub struct Drag -where - R: Fn(&mut V, &mut ViewContext) -> E, - V: 'static, - E: Component<()>, -{ - pub state: S, - pub render_drag_handle: R, - view_type: PhantomData, -} - -impl Drag -where - R: Fn(&mut V, &mut ViewContext) -> E, - V: 'static, - E: Component<()>, -{ - pub fn new(state: S, render_drag_handle: R) -> Self { - Drag { - state, - render_drag_handle, - view_type: PhantomData, - } - } -} - #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] pub enum MouseButton { Left, @@ -194,7 +167,7 @@ impl Deref for MouseExitEvent { pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); impl Render for ExternalPaths { - type Element = Div; + type Element = Div; fn render(&mut self, _: &mut ViewContext) -> Self::Element { div() // Intentionally left empty because the platform will render icons for the dragged files @@ -287,7 +260,7 @@ pub struct FocusEvent { mod test { use crate::{ self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding, - Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext, + Keystroke, ParentComponent, Render, Stateful, TestAppContext, VisualContext, }; struct TestView { @@ -299,20 +272,24 @@ mod test { actions!(TestAction); impl Render for TestView { - type Element = Stateful>; + type Element = Stateful
; - fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { div().id("testview").child( div() .key_context("parent") - .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) - .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true) - .child(|this: &mut Self, _cx: &mut ViewContext| { + .on_key_down(cx.callback(|this, _, _| this.saw_key_down = true)) + .on_action( + cx.callback(|this: &mut TestView, _: &TestAction, _| { + this.saw_action = true + }), + ) + .child( div() .key_context("nested") - .track_focus(&this.focus_handle) - .render() - }), + .track_focus(&self.focus_handle) + .render(), + ), ) } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 1b0cabb401..f958b8b44c 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -2,7 +2,7 @@ use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, - SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, + SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, }; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; @@ -313,7 +313,7 @@ impl Style { } /// Paints the background of an element styled with this style. - pub fn paint(&self, bounds: Bounds, cx: &mut ViewContext) { + pub fn paint(&self, bounds: Bounds, cx: &mut WindowContext) { let rem_size = cx.rem_size(); cx.with_z_index(0, |cx| { diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index c1750d6dc5..7bbbbce7af 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -11,7 +11,7 @@ use std::{ }; pub trait Render: 'static + Sized { - type Element: Element + 'static; + type Element: Element + 'static; fn render(&mut self, cx: &mut ViewContext) -> Self::Element; } @@ -67,7 +67,7 @@ impl View { pub fn render_with(&self, component: C) -> RenderViewWith where - C: 'static + Component, + C: 'static + Component, { RenderViewWith { view: self.clone(), @@ -105,8 +105,8 @@ impl PartialEq for View { impl Eq for View {} -impl Component for View { - fn render(self) -> AnyElement { +impl Component for View { + fn render(self) -> AnyElement { AnyElement::new(AnyView::from(self)) } } @@ -211,8 +211,8 @@ impl AnyView { } } -impl Component for AnyView { - fn render(self) -> AnyElement { +impl Component for AnyView { + fn render(self) -> AnyElement { AnyElement::new(self) } } @@ -227,7 +227,7 @@ impl From> for AnyView { } } -impl Element for AnyView { +impl Element for AnyView { type ElementState = Box; fn element_id(&self) -> Option { @@ -236,9 +236,8 @@ impl Element for AnyView { fn layout( &mut self, - _view_state: &mut ParentViewState, _element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { (self.layout)(self, cx) } @@ -246,9 +245,8 @@ impl Element for AnyView { fn paint( &mut self, _bounds: Bounds, - _view_state: &mut ParentViewState, rendered_element: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { (self.paint)(self, rendered_element, cx) } @@ -281,41 +279,39 @@ impl From> for AnyWeakView { } } -// impl Render for T -// where -// T: 'static + FnMut(&mut WindowContext) -> E, -// E: 'static + Send + Element, -// { -// type Element = E; +impl Render for T +where + T: 'static + FnMut(&mut WindowContext) -> E, + E: 'static + Send + Element, +{ + type Element = E; -// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { -// (self)(cx) -// } -// } + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + (self)(cx) + } +} pub struct RenderViewWith { view: View, component: Option, } -impl Component for RenderViewWith +impl Component for RenderViewWith where - C: 'static + Component, - ParentViewState: 'static, - ViewState: 'static, + V: 'static + Render, + C: 'static + Component, { - fn render(self) -> AnyElement { + fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for RenderViewWith +impl Element for RenderViewWith where - C: 'static + Component, - ParentViewState: 'static, - ViewState: 'static, + V: 'static + Render, + C: 'static + Component, { - type ElementState = AnyElement; + type ElementState = AnyElement; fn element_id(&self) -> Option { Some(self.view.entity_id().into()) @@ -323,25 +319,21 @@ where fn layout( &mut self, - _: &mut ParentViewState, _: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::ElementState) { - self.view.update(cx, |view, cx| { - let mut element = self.component.take().unwrap().render(); - let layout_id = element.layout(view, cx); - (layout_id, element) - }) + let mut element = self.component.take().unwrap().render(); + let layout_id = element.layout(cx); + (layout_id, element) } fn paint( &mut self, _: Bounds, - _: &mut ParentViewState, element: &mut Self::ElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - self.view.update(cx, |view, cx| element.paint(view, cx)) + element.paint(cx) } } @@ -357,7 +349,7 @@ mod any_view { let view = view.clone().downcast::().unwrap(); view.update(cx, |view, cx| { let mut element = AnyElement::new(view.render(cx)); - let layout_id = element.layout(view, cx); + let layout_id = element.layout(cx); (layout_id, Box::new(element) as Box) }) }) @@ -369,9 +361,8 @@ mod any_view { cx: &mut WindowContext, ) { cx.with_element_id(Some(view.model.entity_id), |cx| { - let view = view.clone().downcast::().unwrap(); - let element = element.downcast_mut::>().unwrap(); - view.update(cx, |view, cx| element.paint(view, cx)) + let element = element.downcast_mut::().unwrap(); + element.paint(cx); }) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index a3fe05d39f..d3517996c8 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,15 +1,15 @@ use crate::{ key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, - AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, - DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, - EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, - ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, - ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, - PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, - RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, - Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, - VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, CallbackHandle, ConstructorHandle, + Context, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, + Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, + GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, KeyBinding, KeyContext, + KeyDownEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, + PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, + RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, + SharedString, Size, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, + UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; @@ -1436,6 +1436,47 @@ impl<'a> WindowContext<'a> { .dispatch_tree .bindings_for_action(action) } + + ///========== ELEMENT RELATED FUNCTIONS =========== + pub fn with_key_dispatch( + &mut self, + context: KeyContext, + focus_handle: Option, + f: impl FnOnce(Option, &mut Self) -> R, + ) -> R { + let window = &mut self.window; + window + .current_frame + .dispatch_tree + .push_node(context.clone()); + if let Some(focus_handle) = focus_handle.as_ref() { + window + .current_frame + .dispatch_tree + .make_focusable(focus_handle.id); + } + let result = f(focus_handle, self); + + self.window.current_frame.dispatch_tree.pop_node(); + + result + } + + /// Register a focus listener for the current frame only. It will be cleared + /// on the next frame render. You should use this method only from within elements, + /// and we may want to enforce that better via a different context type. + // todo!() Move this to `FrameContext` to emphasize its individuality? + pub fn on_focus_changed( + &mut self, + listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static, + ) { + self.window + .current_frame + .focus_listeners + .push(Box::new(move |event, cx| { + listener(event, cx); + })); + } } impl Context for WindowContext<'_> { @@ -2124,49 +2165,6 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) } - /// Register a focus listener for the current frame only. It will be cleared - /// on the next frame render. You should use this method only from within elements, - /// and we may want to enforce that better via a different context type. - // todo!() Move this to `FrameContext` to emphasize its individuality? - pub fn on_focus_changed( - &mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) { - let handle = self.view().downgrade(); - self.window - .current_frame - .focus_listeners - .push(Box::new(move |event, cx| { - handle - .update(cx, |view, cx| listener(view, event, cx)) - .log_err(); - })); - } - - pub fn with_key_dispatch( - &mut self, - context: KeyContext, - focus_handle: Option, - f: impl FnOnce(Option, &mut Self) -> R, - ) -> R { - let window = &mut self.window; - window - .current_frame - .dispatch_tree - .push_node(context.clone()); - if let Some(focus_handle) = focus_handle.as_ref() { - window - .current_frame - .dispatch_tree - .make_focusable(focus_handle.id); - } - let result = f(focus_handle, self); - - self.window.current_frame.dispatch_tree.pop_node(); - - result - } - pub fn spawn( &mut self, f: impl FnOnce(WeakView, AsyncWindowContext) -> Fut, @@ -2284,6 +2282,25 @@ impl<'a, V: 'static> ViewContext<'a, V> { { self.defer(|_, cx| cx.emit(Manager::Dismiss)) } + + pub fn callback( + &self, + f: impl Fn(&mut V, &E, &mut ViewContext) + 'static, + ) -> CallbackHandle { + let view = self.view().clone(); + (move |e: &E, cx: &mut WindowContext| { + view.update(cx, |view, cx| f(view, e, cx)); + }) + .into() + } + + pub fn constructor( + &self, + f: impl Fn(&mut V, &mut ViewContext) -> R + 'static, + ) -> ConstructorHandle { + let view = self.view().clone(); + (move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx))).into() + } } impl Context for ViewContext<'_, V> { From f3eb1d4abf192c55c84f3ff4307b162ba69bad8f Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 19 Nov 2023 19:11:51 -0800 Subject: [PATCH 026/205] Add button port example --- crates/ui2/src/components/button.rs | 66 ++++++++++++----------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 1bb611a86e..fdfad8a435 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,25 +1,27 @@ use std::sync::Arc; -use gpui::{DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext}; +use gpui::{ + CallbackHandle, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext, +}; use crate::prelude::*; use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor}; /// Provides the flexibility to use either a standard /// button or an icon button in a given context. -pub enum ButtonOrIconButton { - Button(Button), - IconButton(IconButton), +pub enum ButtonOrIconButton { + Button(Button), + IconButton(IconButton), } -impl From> for ButtonOrIconButton { - fn from(value: Button) -> Self { +impl From