diff --git a/Cargo.lock b/Cargo.lock index 0882435df934b8225238ad1ed19d409a723b4d0d..a7eb358ddb372127ea4e5d0c431d93782e4fd2b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3061,6 +3061,31 @@ dependencies = [ "workspace", ] +[[package]] +name = "file_finder2" +version = "0.1.0" +dependencies = [ + "collections", + "ctor", + "editor2", + "env_logger 0.9.3", + "fuzzy2", + "gpui2", + "language2", + "menu2", + "picker2", + "postage", + "project2", + "serde", + "serde_json", + "settings2", + "text2", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "filetime" version = "0.2.22" @@ -4198,6 +4223,7 @@ dependencies = [ "anyhow", "gpui2", "log", + "serde", "smol", "util", ] @@ -6608,6 +6634,36 @@ dependencies = [ "workspace", ] +[[package]] +name = "project_panel2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "collections", + "context_menu", + "db2", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "menu2", + "postage", + "pretty_assertions", + "project2", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings2", + "smallvec", + "theme2", + "ui2", + "unicase", + "util", + "workspace2", +] + [[package]] name = "project_symbols" version = "0.1.0" @@ -11393,6 +11449,7 @@ dependencies = [ "editor2", "env_logger 0.9.3", "feature_flags2", + "file_finder2", "fs2", "fsevent", "futures 0.3.28", @@ -11402,7 +11459,7 @@ dependencies = [ "ignore", "image", "indexmap 1.9.3", - "install_cli", + "install_cli2", "isahc", "journal2", "language2", @@ -11417,6 +11474,7 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project2", + "project_panel2", "rand 0.8.5", "regex", "rope2", diff --git a/Cargo.toml b/Cargo.toml index 8434acdfd3d595d3cd3c3afd62cba52228e9e75f..6b29b1812759e7d702df950f733b33ed892a4f04 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,6 +80,7 @@ members = [ "crates/project", "crates/project2", "crates/project_panel", + "crates/project_panel2", "crates/project_symbols", "crates/recent_projects", "crates/rope", diff --git a/crates/Cargo.toml b/crates/Cargo.toml deleted file mode 100644 index 6516e07cd43e402b78998330cd7f536f958f5bc5..0000000000000000000000000000000000000000 --- a/crates/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "ai" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/ai.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -gpui = { path = "../gpui" } -util = { path = "../util" } -language = { path = "../language" } -async-trait.workspace = true -anyhow.workspace = true -futures.workspace = true -lazy_static.workspace = true -ordered-float.workspace = true -parking_lot.workspace = true -isahc.workspace = true -regex.workspace = true -serde.workspace = true -serde_json.workspace = true -postage.workspace = true -rand.workspace = true -log.workspace = true -parse_duration = "2.1.1" -tiktoken-rs.workspace = true -matrixmultiply = "0.3.7" -rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } -bincode = "1.3.3" - -[dev-dependencies] -gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 1386186ca410089353f24cea728847e5edfd8372..435a6446693e590f2af9c678df445c0f53b18428 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,9 +1,9 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, - ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, - WeakView, WindowContext, + actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle, + Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView, + WindowContext, }; use picker::{Picker, PickerDelegate}; use std::{ diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index e5c7b0e4a2109a6d10f4859405ef354528bce8b2..7edf8edc8c8e36e3c7f0e10f115561590dc02bda 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -39,12 +39,12 @@ use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - action, actions, div, point, px, relative, rems, render_view, size, uniform_list, AnyElement, - AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, - Entity, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, - InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, - StatefulInteractive, StatelessInteractive, Styled, Subscription, Task, TextStyle, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, + action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list, + AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, + Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, + HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, + Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -7808,17 +7808,22 @@ impl Editor { .pl(cx.anchor_x) .child(render_view( &rename_editor, - EditorElement::new(EditorStyle { - background: cx.theme().system().transparent, - local_player: cx.editor_style.local_player, - text: text_style, - scrollbar_width: cx.editor_style.scrollbar_width, - syntax: cx.editor_style.syntax.clone(), - diagnostic_style: cx - .editor_style - .diagnostic_style - .clone(), - }), + EditorElement::new( + &rename_editor, + EditorStyle { + background: cx.theme().system().transparent, + local_player: cx.editor_style.local_player, + text: text_style, + scrollbar_width: cx + .editor_style + .scrollbar_width, + syntax: cx.editor_style.syntax.clone(), + diagnostic_style: cx + .editor_style + .diagnostic_style + .clone(), + }, + ), )) .render() } @@ -9192,6 +9197,10 @@ impl Editor { cx.focus(&self.focus_handle) } + pub fn is_focused(&self, cx: &WindowContext) -> bool { + self.focus_handle.is_focused(cx) + } + fn handle_focus_in(&mut self, cx: &mut ViewContext) { if self.focus_handle.is_focused(cx) { // todo!() @@ -9403,8 +9412,8 @@ impl Render for Editor { EditorMode::SingleLine => { TextStyle { color: cx.theme().colors().text, - font_family: "Zed Sans".into(), // todo!() - font_features: FontFeatures::default(), + font_family: settings.ui_font.family.clone(), // todo!() + font_features: settings.ui_font.features, font_size: rems(0.875).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, @@ -9433,14 +9442,17 @@ impl Render for Editor { EditorMode::Full => cx.theme().colors().editor_background, }; - EditorElement::new(EditorStyle { - background, - local_player: cx.theme().players().local(), - text: text_style, - scrollbar_width: px(12.), - syntax: cx.theme().syntax().clone(), - diagnostic_style: cx.theme().diagnostic_style(), - }) + EditorElement::new( + cx.view(), + EditorStyle { + background, + local_player: cx.theme().players().local(), + text: text_style, + scrollbar_width: px(12.), + syntax: cx.theme().syntax().clone(), + diagnostic_style: cx.theme().diagnostic_style(), + }, + ) } } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 1ddc5ce7714886fbcd65304d4b21bdb0dd89baa6..289834b50296d10f228e034f6391ace193f9bdde 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -20,9 +20,9 @@ use collections::{BTreeMap, HashMap}; use gpui::{ point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, - ElementInputHandler, Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun, TextStyle, - ViewContext, WindowContext, + ElementInputHandler, Entity, EntityId, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, ParentComponent, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun, + TextStyle, View, ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -111,12 +111,16 @@ impl SelectionLayout { } pub struct EditorElement { + editor_id: EntityId, style: EditorStyle, } impl EditorElement { - pub fn new(style: EditorStyle) -> Self { - Self { style } + pub fn new(editor: &View, style: EditorStyle) -> Self { + Self { + editor_id: editor.entity_id(), + style, + } } fn mouse_down( @@ -622,7 +626,7 @@ impl EditorElement { let line_end_overshoot = 0.15 * layout.position_map.line_height; let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; - cx.with_content_mask(ContentMask { bounds }, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { // todo!("cursor region") // cx.scene().push_cursor_region(CursorRegion { // bounds, @@ -1448,6 +1452,7 @@ impl EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); let font_size = style.text.font_size.to_pixels(cx.rem_size()); let line_height = style.text.line_height_in_pixels(cx.rem_size()); @@ -2399,8 +2404,8 @@ enum Invisible { impl Element for EditorElement { type ElementState = (); - fn id(&self) -> Option { - None + fn element_id(&self) -> Option { + Some(self.editor_id.into()) } fn initialize( @@ -2410,186 +2415,6 @@ impl Element for EditorElement { cx: &mut gpui::ViewContext, ) -> Self::ElementState { editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. - - let dispatch_context = editor.dispatch_context(cx); - cx.with_element_id(cx.view().entity_id(), |global_id, cx| { - cx.with_key_dispatch( - dispatch_context, - Some(editor.focus_handle.clone()), - |_, cx| { - register_action(cx, Editor::move_left); - register_action(cx, Editor::move_right); - register_action(cx, Editor::move_down); - register_action(cx, Editor::move_up); - // on_action(cx, Editor::new_file); todo!() - // on_action(cx, Editor::new_file_in_direction); todo!() - register_action(cx, Editor::cancel); - register_action(cx, Editor::newline); - register_action(cx, Editor::newline_above); - register_action(cx, Editor::newline_below); - register_action(cx, Editor::backspace); - register_action(cx, Editor::delete); - register_action(cx, Editor::tab); - register_action(cx, Editor::tab_prev); - register_action(cx, Editor::indent); - register_action(cx, Editor::outdent); - register_action(cx, Editor::delete_line); - register_action(cx, Editor::join_lines); - register_action(cx, Editor::sort_lines_case_sensitive); - register_action(cx, Editor::sort_lines_case_insensitive); - register_action(cx, Editor::reverse_lines); - register_action(cx, Editor::shuffle_lines); - register_action(cx, Editor::convert_to_upper_case); - register_action(cx, Editor::convert_to_lower_case); - register_action(cx, Editor::convert_to_title_case); - register_action(cx, Editor::convert_to_snake_case); - register_action(cx, Editor::convert_to_kebab_case); - register_action(cx, Editor::convert_to_upper_camel_case); - register_action(cx, Editor::convert_to_lower_camel_case); - register_action(cx, Editor::delete_to_previous_word_start); - register_action(cx, Editor::delete_to_previous_subword_start); - register_action(cx, Editor::delete_to_next_word_end); - register_action(cx, Editor::delete_to_next_subword_end); - register_action(cx, Editor::delete_to_beginning_of_line); - register_action(cx, Editor::delete_to_end_of_line); - register_action(cx, Editor::cut_to_end_of_line); - register_action(cx, Editor::duplicate_line); - register_action(cx, Editor::move_line_up); - register_action(cx, Editor::move_line_down); - register_action(cx, Editor::transpose); - register_action(cx, Editor::cut); - register_action(cx, Editor::copy); - register_action(cx, Editor::paste); - register_action(cx, Editor::undo); - register_action(cx, Editor::redo); - register_action(cx, Editor::move_page_up); - register_action(cx, Editor::move_page_down); - register_action(cx, Editor::next_screen); - register_action(cx, Editor::scroll_cursor_top); - register_action(cx, Editor::scroll_cursor_center); - register_action(cx, Editor::scroll_cursor_bottom); - register_action(cx, |editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }); - register_action(cx, |editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }); - register_action(cx, |editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }); - register_action(cx, |editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }); - register_action(cx, |editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }); - register_action(cx, |editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }); - register_action(cx, Editor::move_to_previous_word_start); - register_action(cx, Editor::move_to_previous_subword_start); - register_action(cx, Editor::move_to_next_word_end); - register_action(cx, Editor::move_to_next_subword_end); - register_action(cx, Editor::move_to_beginning_of_line); - register_action(cx, Editor::move_to_end_of_line); - register_action(cx, Editor::move_to_start_of_paragraph); - register_action(cx, Editor::move_to_end_of_paragraph); - register_action(cx, Editor::move_to_beginning); - register_action(cx, Editor::move_to_end); - register_action(cx, Editor::select_up); - register_action(cx, Editor::select_down); - register_action(cx, Editor::select_left); - register_action(cx, Editor::select_right); - register_action(cx, Editor::select_to_previous_word_start); - register_action(cx, Editor::select_to_previous_subword_start); - register_action(cx, Editor::select_to_next_word_end); - register_action(cx, Editor::select_to_next_subword_end); - register_action(cx, Editor::select_to_beginning_of_line); - register_action(cx, Editor::select_to_end_of_line); - register_action(cx, Editor::select_to_start_of_paragraph); - register_action(cx, Editor::select_to_end_of_paragraph); - register_action(cx, Editor::select_to_beginning); - register_action(cx, Editor::select_to_end); - register_action(cx, Editor::select_all); - register_action(cx, |editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }); - register_action(cx, Editor::select_line); - register_action(cx, Editor::split_selection_into_lines); - register_action(cx, Editor::add_selection_above); - register_action(cx, Editor::add_selection_below); - register_action(cx, |editor, action, cx| { - editor.select_next(action, cx).log_err(); - }); - register_action(cx, |editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }); - register_action(cx, Editor::toggle_comments); - register_action(cx, Editor::select_larger_syntax_node); - register_action(cx, Editor::select_smaller_syntax_node); - register_action(cx, Editor::move_to_enclosing_bracket); - register_action(cx, Editor::undo_selection); - register_action(cx, Editor::redo_selection); - register_action(cx, Editor::go_to_diagnostic); - register_action(cx, Editor::go_to_prev_diagnostic); - register_action(cx, Editor::go_to_hunk); - register_action(cx, Editor::go_to_prev_hunk); - register_action(cx, Editor::go_to_definition); - register_action(cx, Editor::go_to_definition_split); - register_action(cx, Editor::go_to_type_definition); - register_action(cx, Editor::go_to_type_definition_split); - register_action(cx, Editor::fold); - register_action(cx, Editor::fold_at); - register_action(cx, Editor::unfold_lines); - register_action(cx, Editor::unfold_at); - register_action(cx, Editor::fold_selected_ranges); - register_action(cx, Editor::show_completions); - register_action(cx, Editor::toggle_code_actions); - // on_action(cx, Editor::open_excerpts); todo!() - register_action(cx, Editor::toggle_soft_wrap); - register_action(cx, Editor::toggle_inlay_hints); - register_action(cx, Editor::reveal_in_finder); - register_action(cx, Editor::copy_path); - register_action(cx, Editor::copy_relative_path); - register_action(cx, Editor::copy_highlight_json); - register_action(cx, |editor, action, cx| { - editor - .format(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::restart_language_server); - register_action(cx, Editor::show_character_palette); - // on_action(cx, Editor::confirm_completion); todo!() - register_action(cx, |editor, action, cx| { - editor - .confirm_code_action(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .rename(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .confirm_rename(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .find_all_references(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::next_copilot_suggestion); - register_action(cx, Editor::previous_copilot_suggestion); - register_action(cx, Editor::copilot_suggest); - register_action(cx, Editor::context_menu_first); - register_action(cx, Editor::context_menu_prev); - register_action(cx, Editor::context_menu_next); - register_action(cx, Editor::context_menu_last); - }, - ) - }); } fn layout( @@ -2626,32 +2451,208 @@ impl Element for EditorElement { size: layout.text_size, }; - // We call with_z_index to establish a new stacking context. - cx.with_z_index(0, |cx| { - cx.with_content_mask(ContentMask { bounds }, |cx| { - self.paint_mouse_listeners( - bounds, - gutter_bounds, - text_bounds, - &layout.position_map, - cx, - ); - - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, editor, cx); - } - - self.paint_text(text_bounds, &mut layout, editor, cx); + let dispatch_context = editor.dispatch_context(cx); + cx.with_key_dispatch( + dispatch_context, + Some(editor.focus_handle.clone()), + |_, cx| { + register_action(cx, Editor::move_left); + register_action(cx, Editor::move_right); + register_action(cx, Editor::move_down); + register_action(cx, Editor::move_up); + // on_action(cx, Editor::new_file); todo!() + // on_action(cx, Editor::new_file_in_direction); todo!() + register_action(cx, Editor::cancel); + register_action(cx, Editor::newline); + register_action(cx, Editor::newline_above); + register_action(cx, Editor::newline_below); + register_action(cx, Editor::backspace); + register_action(cx, Editor::delete); + register_action(cx, Editor::tab); + register_action(cx, Editor::tab_prev); + register_action(cx, Editor::indent); + register_action(cx, Editor::outdent); + register_action(cx, Editor::delete_line); + register_action(cx, Editor::join_lines); + register_action(cx, Editor::sort_lines_case_sensitive); + register_action(cx, Editor::sort_lines_case_insensitive); + register_action(cx, Editor::reverse_lines); + register_action(cx, Editor::shuffle_lines); + register_action(cx, Editor::convert_to_upper_case); + register_action(cx, Editor::convert_to_lower_case); + register_action(cx, Editor::convert_to_title_case); + register_action(cx, Editor::convert_to_snake_case); + register_action(cx, Editor::convert_to_kebab_case); + register_action(cx, Editor::convert_to_upper_camel_case); + register_action(cx, Editor::convert_to_lower_camel_case); + register_action(cx, Editor::delete_to_previous_word_start); + register_action(cx, Editor::delete_to_previous_subword_start); + register_action(cx, Editor::delete_to_next_word_end); + register_action(cx, Editor::delete_to_next_subword_end); + register_action(cx, Editor::delete_to_beginning_of_line); + register_action(cx, Editor::delete_to_end_of_line); + register_action(cx, Editor::cut_to_end_of_line); + register_action(cx, Editor::duplicate_line); + register_action(cx, Editor::move_line_up); + register_action(cx, Editor::move_line_down); + register_action(cx, Editor::transpose); + register_action(cx, Editor::cut); + register_action(cx, Editor::copy); + register_action(cx, Editor::paste); + register_action(cx, Editor::undo); + register_action(cx, Editor::redo); + register_action(cx, Editor::move_page_up); + register_action(cx, Editor::move_page_down); + register_action(cx, Editor::next_screen); + register_action(cx, Editor::scroll_cursor_top); + register_action(cx, Editor::scroll_cursor_center); + register_action(cx, Editor::scroll_cursor_bottom); + register_action(cx, |editor, _: &LineDown, cx| { + editor.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + register_action(cx, |editor, _: &LineUp, cx| { + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + register_action(cx, |editor, _: &HalfPageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + register_action(cx, |editor, _: &HalfPageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + register_action(cx, |editor, _: &PageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + register_action(cx, |editor, _: &PageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); + register_action(cx, Editor::move_to_previous_word_start); + register_action(cx, Editor::move_to_previous_subword_start); + register_action(cx, Editor::move_to_next_word_end); + register_action(cx, Editor::move_to_next_subword_end); + register_action(cx, Editor::move_to_beginning_of_line); + register_action(cx, Editor::move_to_end_of_line); + register_action(cx, Editor::move_to_start_of_paragraph); + register_action(cx, Editor::move_to_end_of_paragraph); + register_action(cx, Editor::move_to_beginning); + register_action(cx, Editor::move_to_end); + register_action(cx, Editor::select_up); + register_action(cx, Editor::select_down); + register_action(cx, Editor::select_left); + register_action(cx, Editor::select_right); + register_action(cx, Editor::select_to_previous_word_start); + register_action(cx, Editor::select_to_previous_subword_start); + register_action(cx, Editor::select_to_next_word_end); + register_action(cx, Editor::select_to_next_subword_end); + register_action(cx, Editor::select_to_beginning_of_line); + register_action(cx, Editor::select_to_end_of_line); + register_action(cx, Editor::select_to_start_of_paragraph); + register_action(cx, Editor::select_to_end_of_paragraph); + register_action(cx, Editor::select_to_beginning); + register_action(cx, Editor::select_to_end); + register_action(cx, Editor::select_all); + register_action(cx, |editor, action, cx| { + editor.select_all_matches(action, cx).log_err(); + }); + register_action(cx, Editor::select_line); + register_action(cx, Editor::split_selection_into_lines); + register_action(cx, Editor::add_selection_above); + register_action(cx, Editor::add_selection_below); + register_action(cx, |editor, action, cx| { + editor.select_next(action, cx).log_err(); + }); + register_action(cx, |editor, action, cx| { + editor.select_previous(action, cx).log_err(); + }); + register_action(cx, Editor::toggle_comments); + register_action(cx, Editor::select_larger_syntax_node); + register_action(cx, Editor::select_smaller_syntax_node); + register_action(cx, Editor::move_to_enclosing_bracket); + register_action(cx, Editor::undo_selection); + register_action(cx, Editor::redo_selection); + register_action(cx, Editor::go_to_diagnostic); + register_action(cx, Editor::go_to_prev_diagnostic); + register_action(cx, Editor::go_to_hunk); + register_action(cx, Editor::go_to_prev_hunk); + register_action(cx, Editor::go_to_definition); + register_action(cx, Editor::go_to_definition_split); + register_action(cx, Editor::go_to_type_definition); + register_action(cx, Editor::go_to_type_definition_split); + register_action(cx, Editor::fold); + register_action(cx, Editor::fold_at); + register_action(cx, Editor::unfold_lines); + register_action(cx, Editor::unfold_at); + register_action(cx, Editor::fold_selected_ranges); + register_action(cx, Editor::show_completions); + register_action(cx, Editor::toggle_code_actions); + // on_action(cx, Editor::open_excerpts); todo!() + register_action(cx, Editor::toggle_soft_wrap); + register_action(cx, Editor::toggle_inlay_hints); + register_action(cx, Editor::reveal_in_finder); + register_action(cx, Editor::copy_path); + register_action(cx, Editor::copy_relative_path); + register_action(cx, Editor::copy_highlight_json); + register_action(cx, |editor, action, cx| { + editor + .format(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, Editor::restart_language_server); + register_action(cx, Editor::show_character_palette); + // on_action(cx, Editor::confirm_completion); todo!() + register_action(cx, |editor, action, cx| { + editor + .confirm_code_action(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .confirm_rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, |editor, action, cx| { + editor + .find_all_references(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(cx, Editor::next_copilot_suggestion); + register_action(cx, Editor::previous_copilot_suggestion); + register_action(cx, Editor::copilot_suggest); + register_action(cx, Editor::context_menu_first); + register_action(cx, Editor::context_menu_prev); + register_action(cx, Editor::context_menu_next); + register_action(cx, Editor::context_menu_last); + + // We call with_z_index to establish a new stacking context. + cx.with_z_index(0, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout.position_map, + cx, + ); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, editor, cx); + } + self.paint_text(text_bounds, &mut layout, editor, cx); - if !layout.blocks.is_empty() { - self.paint_blocks(bounds, &mut layout, editor, cx); - } + if !layout.blocks.is_empty() { + self.paint_blocks(bounds, &mut layout, editor, cx); + } - let input_handler = ElementInputHandler::new(bounds, cx); - cx.handle_input(&editor.focus_handle, input_handler); - }); - }); + let input_handler = ElementInputHandler::new(bounds, cx); + cx.handle_input(&editor.focus_handle, input_handler); + }); + }); + }, + ) } } diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 9614082ccf240601eb85a0bd9166f09a4db5e790..09bb2f1182549478baa0250ce1425c5e2ea61b27 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, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, + FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use language::{ @@ -30,7 +30,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{Label, LabelColor}; +use ui::{Label, TextColor}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::{ @@ -607,7 +607,7 @@ impl Item for Editor { &description, MAX_TAB_TITLE_LEN, )) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) })), diff --git a/crates/file_finder2/Cargo.toml b/crates/file_finder2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..22b9f2cbc825d7205dfbdfcd8bf34930c393b41c --- /dev/null +++ b/crates/file_finder2/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "file_finder2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/file_finder.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +collections = { path = "../collections" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +menu = { package = "menu2", path = "../menu2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +text = { package = "text2", path = "../text2" } +util = { path = "../util" } +theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } +workspace = { package = "workspace2", path = "../workspace2" } +postage.workspace = true +serde.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +theme = { package = "theme2", path = "../theme2", features = ["test-support"] } + +serde_json.workspace = true +ctor.workspace = true +env_logger.workspace = true diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs new file mode 100644 index 0000000000000000000000000000000000000000..aae3bca160da8d9db514e2e566ff7d4a1dbeeb09 --- /dev/null +++ b/crates/file_finder2/src/file_finder.rs @@ -0,0 +1,1973 @@ +use collections::HashMap; +use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; +use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; +use gpui::{ + actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model, + ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, + WindowContext, +}; +use picker::{Picker, PickerDelegate}; +use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; +use std::{ + path::{Path, PathBuf}, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, +}; +use text::Point; +use theme::ActiveTheme; +use ui::{v_stack, HighlightedLabel, StyledExt}; +use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; +use workspace::{Modal, ModalEvent, Workspace}; + +actions!(Toggle); + +pub struct FileFinder { + picker: View>, +} + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(FileFinder::register).detach(); +} + +impl FileFinder { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &Toggle, cx| { + let Some(file_finder) = workspace.current_modal::(cx) else { + Self::open(workspace, cx); + return; + }; + + file_finder.update(cx, |file_finder, cx| { + file_finder + .picker + .update(cx, |picker, cx| picker.cycle_selection(cx)) + }); + }); + } + + fn open(workspace: &mut Workspace, cx: &mut ViewContext) { + let project = workspace.project().read(cx); + + let currently_opened_path = workspace + .active_item(cx) + .and_then(|item| item.project_path(cx)) + .map(|project_path| { + let abs_path = project + .worktree_for_id(project_path.worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path)); + FoundPath::new(project_path, abs_path) + }); + + // if exists, bubble the currently opened path to the top + let history_items = currently_opened_path + .clone() + .into_iter() + .chain( + workspace + .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx) + .into_iter() + .filter(|(history_path, _)| { + Some(history_path) + != currently_opened_path + .as_ref() + .map(|found_path| &found_path.project) + }) + .filter(|(_, history_abs_path)| { + history_abs_path.as_ref() + != currently_opened_path + .as_ref() + .and_then(|found_path| found_path.absolute.as_ref()) + }) + .filter(|(_, history_abs_path)| match history_abs_path { + Some(abs_path) => history_file_exists(abs_path), + None => true, + }) + .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)), + ) + .collect(); + + let project = workspace.project().clone(); + let weak_workspace = cx.view().downgrade(); + workspace.toggle_modal(cx, |cx| { + let delegate = FileFinderDelegate::new( + cx.view().downgrade(), + weak_workspace, + project, + currently_opened_path, + history_items, + cx, + ); + + FileFinder::new(delegate, cx) + }); + } + + fn new(delegate: FileFinderDelegate, cx: &mut ViewContext) -> Self { + Self { + picker: cx.build_view(|cx| Picker::new(delegate, cx)), + } + } +} + +impl EventEmitter for FileFinder {} +impl Modal for FileFinder { + fn focus(&self, cx: &mut WindowContext) { + self.picker.update(cx, |picker, cx| picker.focus(cx)) + } +} +impl Render for FileFinder { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().w_96().child(self.picker.clone()) + } +} + +pub struct FileFinderDelegate { + file_finder: WeakView, + workspace: WeakView, + project: Model, + search_count: usize, + latest_search_id: usize, + latest_search_did_cancel: bool, + latest_search_query: Option>, + currently_opened_path: Option, + matches: Matches, + selected_index: Option, + cancel_flag: Arc, + history_items: Vec, +} + +#[derive(Debug, Default)] +struct Matches { + history: Vec<(FoundPath, Option)>, + search: Vec, +} + +#[derive(Debug)] +enum Match<'a> { + History(&'a FoundPath, Option<&'a PathMatch>), + Search(&'a PathMatch), +} + +impl Matches { + fn len(&self) -> usize { + self.history.len() + self.search.len() + } + + fn get(&self, index: usize) -> Option> { + if index < self.history.len() { + self.history + .get(index) + .map(|(path, path_match)| Match::History(path, path_match.as_ref())) + } else { + self.search + .get(index - self.history.len()) + .map(Match::Search) + } + } + + fn push_new_matches( + &mut self, + history_items: &Vec, + query: &PathLikeWithPosition, + mut new_search_matches: Vec, + extend_old_matches: bool, + ) { + let matching_history_paths = matching_history_item_paths(history_items, query); + new_search_matches + .retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); + let history_items_to_show = history_items + .iter() + .filter_map(|history_item| { + Some(( + history_item.clone(), + Some( + matching_history_paths + .get(&history_item.project.path)? + .clone(), + ), + )) + }) + .collect::>(); + self.history = history_items_to_show; + if extend_old_matches { + self.search + .retain(|path_match| !matching_history_paths.contains_key(&path_match.path)); + util::extend_sorted( + &mut self.search, + new_search_matches.into_iter(), + 100, + |a, b| b.cmp(a), + ) + } else { + self.search = new_search_matches; + } + } +} + +fn matching_history_item_paths( + history_items: &Vec, + query: &PathLikeWithPosition, +) -> HashMap, PathMatch> { + let history_items_by_worktrees = history_items + .iter() + .filter_map(|found_path| { + let candidate = PathMatchCandidate { + path: &found_path.project.path, + // Only match history items names, otherwise their paths may match too many queries, producing false positives. + // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item, + // it would be shown first always, despite the latter being a better match. + char_bag: CharBag::from_iter( + found_path + .project + .path + .file_name()? + .to_string_lossy() + .to_lowercase() + .chars(), + ), + }; + Some((found_path.project.worktree_id, candidate)) + }) + .fold( + HashMap::default(), + |mut candidates, (worktree_id, new_candidate)| { + candidates + .entry(worktree_id) + .or_insert_with(Vec::new) + .push(new_candidate); + candidates + }, + ); + let mut matching_history_paths = HashMap::default(); + for (worktree, candidates) in history_items_by_worktrees { + let max_results = candidates.len() + 1; + matching_history_paths.extend( + fuzzy::match_fixed_path_set( + candidates, + worktree.to_usize(), + query.path_like.path_query(), + false, + max_results, + ) + .into_iter() + .map(|path_match| (Arc::clone(&path_match.path), path_match)), + ); + } + matching_history_paths +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct FoundPath { + project: ProjectPath, + absolute: Option, +} + +impl FoundPath { + fn new(project: ProjectPath, absolute: Option) -> Self { + Self { project, absolute } + } +} + +const MAX_RECENT_SELECTIONS: usize = 20; + +#[cfg(not(test))] +fn history_file_exists(abs_path: &PathBuf) -> bool { + abs_path.exists() +} + +#[cfg(test)] +fn history_file_exists(abs_path: &PathBuf) -> bool { + !abs_path.ends_with("nonexistent.rs") +} + +pub enum Event { + Selected(ProjectPath), + Dismissed, +} + +#[derive(Debug, Clone)] +struct FileSearchQuery { + raw_query: String, + file_query_end: Option, +} + +impl FileSearchQuery { + fn path_query(&self) -> &str { + match self.file_query_end { + Some(file_path_end) => &self.raw_query[..file_path_end], + None => &self.raw_query, + } + } +} + +impl FileFinderDelegate { + fn new( + file_finder: WeakView, + workspace: WeakView, + project: Model, + currently_opened_path: Option, + history_items: Vec, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&project, |file_finder, _, cx| { + //todo!() We should probably not re-render on every project anything + file_finder + .picker + .update(cx, |picker, cx| picker.refresh(cx)) + }) + .detach(); + + Self { + file_finder, + workspace, + project, + search_count: 0, + latest_search_id: 0, + latest_search_did_cancel: false, + latest_search_query: None, + currently_opened_path, + matches: Matches::default(), + selected_index: None, + cancel_flag: Arc::new(AtomicBool::new(false)), + history_items, + } + } + + fn spawn_search( + &mut self, + query: PathLikeWithPosition, + cx: &mut ViewContext>, + ) -> Task<()> { + let relative_to = self + .currently_opened_path + .as_ref() + .map(|found_path| Arc::clone(&found_path.project.path)); + let worktrees = self + .project + .read(cx) + .visible_worktrees(cx) + .collect::>(); + let include_root_name = worktrees.len() > 1; + let candidate_sets = worktrees + .into_iter() + .map(|worktree| { + let worktree = worktree.read(cx); + PathMatchCandidateSet { + snapshot: worktree.snapshot(), + include_ignored: worktree + .root_entry() + .map_or(false, |entry| entry.is_ignored), + include_root_name, + } + }) + .collect::>(); + + let search_id = util::post_inc(&mut self.search_count); + self.cancel_flag.store(true, atomic::Ordering::Relaxed); + self.cancel_flag = Arc::new(AtomicBool::new(false)); + let cancel_flag = self.cancel_flag.clone(); + cx.spawn(|picker, mut cx| async move { + let matches = fuzzy::match_path_sets( + candidate_sets.as_slice(), + query.path_like.path_query(), + relative_to, + false, + 100, + &cancel_flag, + cx.background_executor().clone(), + ) + .await; + let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); + picker + .update(&mut cx, |picker, cx| { + picker + .delegate + .set_search_matches(search_id, did_cancel, query, matches, cx) + }) + .log_err(); + }) + } + + fn set_search_matches( + &mut self, + search_id: usize, + did_cancel: bool, + query: PathLikeWithPosition, + matches: Vec, + cx: &mut ViewContext>, + ) { + if search_id >= self.latest_search_id { + self.latest_search_id = search_id; + let extend_old_matches = self.latest_search_did_cancel + && Some(query.path_like.path_query()) + == self + .latest_search_query + .as_ref() + .map(|query| query.path_like.path_query()); + self.matches + .push_new_matches(&self.history_items, &query, matches, extend_old_matches); + self.latest_search_query = Some(query); + self.latest_search_did_cancel = did_cancel; + cx.notify(); + } + } + + fn labels_for_match( + &self, + path_match: Match, + cx: &AppContext, + ix: usize, + ) -> (String, Vec, String, Vec) { + let (file_name, file_name_positions, full_path, full_path_positions) = match path_match { + Match::History(found_path, found_path_match) => { + let worktree_id = found_path.project.worktree_id; + let project_relative_path = &found_path.project.path; + let has_worktree = self + .project + .read(cx) + .worktree_for_id(worktree_id, cx) + .is_some(); + + if !has_worktree { + if let Some(absolute_path) = &found_path.absolute { + return ( + absolute_path + .file_name() + .map_or_else( + || project_relative_path.to_string_lossy(), + |file_name| file_name.to_string_lossy(), + ) + .to_string(), + Vec::new(), + absolute_path.to_string_lossy().to_string(), + Vec::new(), + ); + } + } + + let mut path = Arc::clone(project_relative_path); + if project_relative_path.as_ref() == Path::new("") { + if let Some(absolute_path) = &found_path.absolute { + path = Arc::from(absolute_path.as_path()); + } + } + + let mut path_match = PathMatch { + score: ix as f64, + positions: Vec::new(), + worktree_id: worktree_id.to_usize(), + path, + path_prefix: "".into(), + distance_to_relative_ancestor: usize::MAX, + }; + if let Some(found_path_match) = found_path_match { + path_match + .positions + .extend(found_path_match.positions.iter()) + } + + self.labels_for_path_match(&path_match) + } + Match::Search(path_match) => self.labels_for_path_match(path_match), + }; + + if file_name_positions.is_empty() { + if let Some(user_home_path) = std::env::var("HOME").ok() { + let user_home_path = user_home_path.trim(); + if !user_home_path.is_empty() { + if (&full_path).starts_with(user_home_path) { + return ( + file_name, + file_name_positions, + full_path.replace(user_home_path, "~"), + full_path_positions, + ); + } + } + } + } + + ( + file_name, + file_name_positions, + full_path, + full_path_positions, + ) + } + + fn labels_for_path_match( + &self, + path_match: &PathMatch, + ) -> (String, Vec, String, Vec) { + let path = &path_match.path; + let path_string = path.to_string_lossy(); + let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join(""); + let path_positions = path_match.positions.clone(); + + let file_name = path.file_name().map_or_else( + || path_match.path_prefix.to_string(), + |file_name| file_name.to_string_lossy().to_string(), + ); + let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count() + - file_name.chars().count(); + let file_name_positions = path_positions + .iter() + .filter_map(|pos| { + if pos >= &file_name_start { + Some(pos - file_name_start) + } else { + None + } + }) + .collect(); + + (file_name, file_name_positions, full_path, path_positions) + } +} + +impl PickerDelegate for FileFinderDelegate { + type ListItem = Div>; + + fn placeholder_text(&self) -> Arc { + "Search project files...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index.unwrap_or(0) + } + + fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>) { + self.selected_index = Some(ix); + cx.notify(); + } + + fn update_matches( + &mut self, + raw_query: String, + cx: &mut ViewContext>, + ) -> Task<()> { + if raw_query.is_empty() { + let project = self.project.read(cx); + self.latest_search_id = post_inc(&mut self.search_count); + self.matches = Matches { + history: self + .history_items + .iter() + .filter(|history_item| { + project + .worktree_for_id(history_item.project.worktree_id, cx) + .is_some() + || (project.is_local() && history_item.absolute.is_some()) + }) + .cloned() + .map(|p| (p, None)) + .collect(), + search: Vec::new(), + }; + cx.notify(); + Task::ready(()) + } else { + let raw_query = &raw_query; + let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| { + Ok::<_, std::convert::Infallible>(FileSearchQuery { + raw_query: raw_query.to_owned(), + file_query_end: if path_like_str == raw_query { + None + } else { + Some(path_like_str.len()) + }, + }) + }) + .expect("infallible"); + self.spawn_search(query, cx) + } + } + + fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>) { + if let Some(m) = self.matches.get(self.selected_index()) { + if let Some(workspace) = self.workspace.upgrade() { + let open_task = workspace.update(cx, move |workspace, cx| { + let split_or_open = |workspace: &mut Workspace, project_path, cx| { + if secondary { + workspace.split_path(project_path, cx) + } else { + workspace.open_path(project_path, None, true, cx) + } + }; + match m { + Match::History(history_match, _) => { + let worktree_id = history_match.project.worktree_id; + if workspace + .project() + .read(cx) + .worktree_for_id(worktree_id, cx) + .is_some() + { + split_or_open( + workspace, + ProjectPath { + worktree_id, + path: Arc::clone(&history_match.project.path), + }, + cx, + ) + } else { + match history_match.absolute.as_ref() { + Some(abs_path) => { + if secondary { + workspace.split_abs_path( + abs_path.to_path_buf(), + false, + cx, + ) + } else { + workspace.open_abs_path( + abs_path.to_path_buf(), + false, + cx, + ) + } + } + None => split_or_open( + workspace, + ProjectPath { + worktree_id, + path: Arc::clone(&history_match.project.path), + }, + cx, + ), + } + } + } + Match::Search(m) => split_or_open( + workspace, + ProjectPath { + worktree_id: WorktreeId::from_usize(m.worktree_id), + path: m.path.clone(), + }, + cx, + ), + } + }); + + let row = self + .latest_search_query + .as_ref() + .and_then(|query| query.row) + .map(|row| row.saturating_sub(1)); + let col = self + .latest_search_query + .as_ref() + .and_then(|query| query.column) + .unwrap_or(0) + .saturating_sub(1); + let finder = self.file_finder.clone(); + + cx.spawn(|_, mut cx| async move { + let item = open_task.await.log_err()?; + if let Some(row) = row { + if let Some(active_editor) = item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot + .buffer_snapshot + .clip_point(Point::new(row, col), Bias::Left); + editor.change_selections(Some(Autoscroll::center()), cx, |s| { + s.select_ranges([point..point]) + }); + }) + .log_err(); + } + } + finder + .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .ok()?; + + Some(()) + }) + .detach(); + } + } + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.file_finder + .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .log_err(); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + cx: &mut ViewContext>, + ) -> Self::ListItem { + let path_match = self + .matches + .get(ix) + .expect("Invalid matches state: no element for index {ix}"); + let theme = cx.theme(); + let colors = theme.colors(); + + let (file_name, file_name_positions, full_path, full_path_positions) = + self.labels_for_match(path_match, cx, ix); + + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child( + v_stack() + .child(HighlightedLabel::new(file_name, file_name_positions)) + .child(HighlightedLabel::new(full_path, full_path_positions)), + ) + } +} + +// #[cfg(test)] +// mod tests { +// use std::{assert_eq, collections::HashMap, path::Path, time::Duration}; + +// use super::*; +// use editor::Editor; +// use gpui::{Entity, TestAppContext, VisualTestContext}; +// use menu::{Confirm, SelectNext}; +// use serde_json::json; +// use workspace::{AppState, Workspace}; + +// #[ctor::ctor] +// fn init_logger() { +// if std::env::var("RUST_LOG").is_ok() { +// env_logger::init(); +// } +// } + +// #[gpui::test] +// async fn test_matching_paths(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "a": { +// "banana": "", +// "bandana": "", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; + +// let (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// picker +// .update(cx, |picker, cx| { +// picker.delegate.update_matches("bna".to_string(), cx) +// }) +// .await; + +// picker.update(cx, |picker, _| { +// assert_eq!(picker.delegate.matches.len(), 2); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// cx.read(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// assert_eq!( +// active_item +// .to_any() +// .downcast::() +// .unwrap() +// .read(cx) +// .title(cx), +// "bandana" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let first_file_name = "first.rs"; +// let first_file_contents = "// First Rust file"; +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// first_file_name: first_file_contents, +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + +// let (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// let file_query = &first_file_name[..3]; +// let file_row = 1; +// let file_column = 3; +// assert!(file_column <= first_file_contents.len()); +// let query_inside_file = format!("{file_query}:{file_row}:{file_column}"); +// picker +// .update(cx, |finder, cx| { +// finder +// .delegate +// .update_matches(query_inside_file.to_string(), cx) +// }) +// .await; +// picker.update(cx, |finder, _| { +// let finder = &finder.delegate; +// assert_eq!(finder.matches.len(), 1); +// let latest_search_query = finder +// .latest_search_query +// .as_ref() +// .expect("Finder should have a query after the update_matches call"); +// assert_eq!(latest_search_query.path_like.raw_query, query_inside_file); +// assert_eq!( +// latest_search_query.path_like.file_query_end, +// Some(file_query.len()) +// ); +// assert_eq!(latest_search_query.row, Some(file_row)); +// assert_eq!(latest_search_query.column, Some(file_column as u32)); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// let editor = cx.update(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// active_item.downcast::().unwrap() +// }); +// cx.executor().advance_clock(Duration::from_secs(2)); + +// editor.update(cx, |editor, cx| { +// let all_selections = editor.selections.all_adjusted(cx); +// assert_eq!( +// all_selections.len(), +// 1, +// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" +// ); +// let caret_selection = all_selections.into_iter().next().unwrap(); +// assert_eq!(caret_selection.start, caret_selection.end, +// "Caret selection should have its start and end at the same position"); +// assert_eq!(file_row, caret_selection.start.row + 1, +// "Query inside file should get caret with the same focus row"); +// assert_eq!(file_column, caret_selection.start.column as usize + 1, +// "Query inside file should get caret with the same focus column"); +// }); +// } + +// #[gpui::test] +// async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) { +// let app_state = init_test(cx); + +// let first_file_name = "first.rs"; +// let first_file_contents = "// First Rust file"; +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// first_file_name: first_file_contents, +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + +// let (picker, workspace, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// let file_query = &first_file_name[..3]; +// let file_row = 200; +// let file_column = 300; +// assert!(file_column > first_file_contents.len()); +// let query_outside_file = format!("{file_query}:{file_row}:{file_column}"); +// picker +// .update(cx, |picker, cx| { +// picker +// .delegate +// .update_matches(query_outside_file.to_string(), cx) +// }) +// .await; +// picker.update(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert_eq!(delegate.matches.len(), 1); +// let latest_search_query = delegate +// .latest_search_query +// .as_ref() +// .expect("Finder should have a query after the update_matches call"); +// assert_eq!(latest_search_query.path_like.raw_query, query_outside_file); +// assert_eq!( +// latest_search_query.path_like.file_query_end, +// Some(file_query.len()) +// ); +// assert_eq!(latest_search_query.row, Some(file_row)); +// assert_eq!(latest_search_query.column, Some(file_column as u32)); +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(Confirm); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// let editor = cx.update(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// active_item.downcast::().unwrap() +// }); +// cx.executor().advance_clock(Duration::from_secs(2)); + +// editor.update(cx, |editor, cx| { +// let all_selections = editor.selections.all_adjusted(cx); +// assert_eq!( +// all_selections.len(), +// 1, +// "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}" +// ); +// let caret_selection = all_selections.into_iter().next().unwrap(); +// assert_eq!(caret_selection.start, caret_selection.end, +// "Caret selection should have its start and end at the same position"); +// assert_eq!(0, caret_selection.start.row, +// "Excessive rows (as in query outside file borders) should get trimmed to last file row"); +// assert_eq!(first_file_contents.len(), caret_selection.start.column as usize, +// "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column"); +// }); +// } + +// #[gpui::test] +// async fn test_matching_cancellation(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/dir", +// json!({ +// "hello": "", +// "goodbye": "", +// "halogen-light": "", +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; + +// let (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// let query = test_path_like("hi"); +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(query.clone(), cx) +// }) +// .await; + +// picker.update(cx, |picker, _cx| { +// assert_eq!(picker.delegate.matches.len(), 5) +// }); + +// picker.update(cx, |picker, cx| { +// let delegate = &mut picker.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); + +// // Simulate a search being cancelled after the time limit, +// // returning only a subset of the matches that would have been found. +// drop(delegate.spawn_search(query.clone(), cx)); +// delegate.set_search_matches( +// delegate.latest_search_id, +// true, // did-cancel +// query.clone(), +// vec![matches[1].clone(), matches[3].clone()], +// cx, +// ); + +// // Simulate another cancellation. +// drop(delegate.spawn_search(query.clone(), cx)); +// delegate.set_search_matches( +// delegate.latest_search_id, +// true, // did-cancel +// query.clone(), +// vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], +// cx, +// ); + +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]); +// }); +// } + +// #[gpui::test] +// async fn test_ignored_files(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/ancestor", +// json!({ +// ".gitignore": "ignored-root", +// "ignored-root": { +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }, +// "tracked-root": { +// ".gitignore": "height", +// "happiness": "", +// "height": "", +// "hi": "", +// "hiccup": "", +// }, +// }), +// ) +// .await; + +// let project = Project::test( +// app_state.fs.clone(), +// [ +// "/ancestor/tracked-root".as_ref(), +// "/ancestor/ignored-root".as_ref(), +// ], +// cx, +// ) +// .await; + +// let (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(test_path_like("hi"), cx) +// }) +// .await; +// picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7)); +// } + +// #[gpui::test] +// async fn test_single_file_worktrees(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } })) +// .await; + +// let project = Project::test( +// app_state.fs.clone(), +// ["/root/the-parent-dir/the-file".as_ref()], +// cx, +// ) +// .await; + +// let (picker, _, mut cx) = build_find_picker(project, cx); +// let cx = &mut cx; + +// // Even though there is only one worktree, that worktree's filename +// // is included in the matching, because the worktree is a single file. +// picker +// .update(cx, |picker, cx| { +// picker.delegate.spawn_search(test_path_like("thf"), cx) +// }) +// .await; +// cx.read(|cx| { +// let picker = picker.read(cx); +// let delegate = &picker.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); +// assert_eq!(matches.len(), 1); + +// let (file_name, file_name_positions, full_path, full_path_positions) = +// delegate.labels_for_path_match(&matches[0]); +// assert_eq!(file_name, "the-file"); +// assert_eq!(file_name_positions, &[0, 1, 4]); +// assert_eq!(full_path, "the-file"); +// assert_eq!(full_path_positions, &[0, 1, 4]); +// }); + +// // Since the worktree root is a file, searching for its name followed by a slash does +// // not match anything. +// picker +// .update(cx, |f, cx| { +// f.delegate.spawn_search(test_path_like("thf/"), cx) +// }) +// .await; +// picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0)); +// } + +// #[gpui::test] +// async fn test_path_distance_ordering(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "dir1": { "a.txt": "" }, +// "dir2": { +// "a.txt": "", +// "b.txt": "" +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; + +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// WorktreeId::from_usize(worktrees[0].id()) +// }); + +// // When workspace has an active item, sort items which are closer to that item +// // first when they have the same name. In this case, b.txt is closer to dir2's a.txt +// // so that one should be sorted earlier +// let b_path = Some(dummy_found_path(ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("/root/dir2/b.txt")), +// })); +// cx.dispatch_action(Toggle); + +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// b_path, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); + +// finder +// .update(cx, |f, cx| { +// f.delegate.spawn_search(test_path_like("a.txt"), cx) +// }) +// .await; + +// finder.read_with(cx, |f, _| { +// let delegate = &f.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "Search matches expected" +// ); +// let matches = delegate.matches.search.clone(); +// assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt")); +// assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt")); +// }); +// } + +// #[gpui::test] +// async fn test_search_worktree_without_files(cx: &mut TestAppContext) { +// let app_state = init_test(cx); +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/root", +// json!({ +// "dir1": {}, +// "dir2": { +// "dir3": {} +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project, cx)) +// .root(cx); +// let finder = cx +// .add_window(|cx| { +// Picker::new( +// FileFinderDelegate::new( +// workspace.downgrade(), +// workspace.read(cx).project().clone(), +// None, +// Vec::new(), +// cx, +// ), +// cx, +// ) +// }) +// .root(cx); +// finder +// .update(cx, |f, cx| { +// f.delegate.spawn_search(test_path_like("dir"), cx) +// }) +// .await; +// cx.read(|cx| { +// let finder = finder.read(cx); +// assert_eq!(finder.delegate.matches.len(), 0); +// }); +// } + +// #[gpui::test] +// async fn test_query_history(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// WorktreeId::from_usize(worktrees[0].id()) +// }); + +// // Open and close panels, getting their history items afterwards. +// // Ensure history items get populated with opened items, and items are kept in a certain order. +// // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen. +// // +// // TODO: without closing, the opened items do not propagate their history changes for some reason +// // it does work in real app though, only tests do not propagate. + +// let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// assert!( +// initial_history.is_empty(), +// "Should have no history before opening any files" +// ); + +// let history_after_first = +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// assert_eq!( +// history_after_first, +// vec![FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )], +// "Should show 1st opened item in the history when opening the 2nd item" +// ); + +// let history_after_second = +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// assert_eq!( +// history_after_second, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st and 2nd opened items in the history when opening the 3rd item. \ +// 2nd item should be the first in the history, as the last opened." +// ); + +// let history_after_third = +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// assert_eq!( +// history_after_third, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/third.rs")), +// }, +// Some(PathBuf::from("/src/test/third.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \ +// 3rd item should be the first in the history, as the last opened." +// ); + +// let history_after_second_again = +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// assert_eq!( +// history_after_second_again, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/third.rs")), +// }, +// Some(PathBuf::from("/src/test/third.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// ), +// ], +// "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \ +// 2nd item, as the last opened, 3rd item should go next as it was opened right before." +// ); +// } + +// #[gpui::test] +// async fn test_external_files_history(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// } +// }), +// ) +// .await; + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/external-src", +// json!({ +// "test": { +// "third.rs": "// Third Rust file", +// "fourth.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// cx.update(|cx| { +// project.update(cx, |project, cx| { +// project.find_or_create_local_worktree("/external-src", false, cx) +// }) +// }) +// .detach(); +// cx.background_executor.run_until_parked(); + +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1,); + +// WorktreeId::from_usize(worktrees[0].id()) +// }); +// workspace +// .update(cx, |workspace, cx| { +// workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx) +// }) +// .detach(); +// cx.background_executor.run_until_parked(); +// let external_worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!( +// worktrees.len(), +// 2, +// "External file should get opened in a new worktree" +// ); + +// WorktreeId::from_usize( +// worktrees +// .into_iter() +// .find(|worktree| worktree.entity_id() != worktree_id.to_usize()) +// .expect("New worktree should have a different id") +// .id(), +// ) +// }); +// close_active_item(&workspace, cx).await; + +// let initial_history_items = +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// assert_eq!( +// initial_history_items, +// vec![FoundPath::new( +// ProjectPath { +// worktree_id: external_worktree_id, +// path: Arc::from(Path::new("")), +// }, +// Some(PathBuf::from("/external-src/test/third.rs")) +// )], +// "Should show external file with its full path in the history after it was open" +// ); + +// let updated_history_items = +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// assert_eq!( +// updated_history_items, +// vec![ +// FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/second.rs")), +// }, +// Some(PathBuf::from("/src/test/second.rs")) +// ), +// FoundPath::new( +// ProjectPath { +// worktree_id: external_worktree_id, +// path: Arc::from(Path::new("")), +// }, +// Some(PathBuf::from("/external-src/test/third.rs")) +// ), +// ], +// "Should keep external file with history updates", +// ); +// } + +// #[gpui::test] +// async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; + +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// cx.executor().run_until_parked(); +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// for expected_selected_index in 0..current_history.len() { +// cx.dispatch_action(Toggle); +// let selected_index = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .read(cx) +// .delegate +// .selected_index() +// }); +// assert_eq!( +// selected_index, expected_selected_index, +// "Should select the next item in the history" +// ); +// } + +// cx.dispatch_action(Toggle); +// let selected_index = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .read(cx) +// .delegate +// .selected_index() +// }); +// assert_eq!( +// selected_index, 0, +// "Should wrap around the history and start all over" +// ); +// } + +// #[gpui::test] +// async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// "fourth.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// let worktree_id = cx.read(|cx| { +// let worktrees = workspace.read(cx).worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1,); + +// WorktreeId::from_usize(worktrees[0].entity_id()) +// }); + +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let first_query = "f"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(first_query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out"); +// let history_match = delegate.matches.history.first().unwrap(); +// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); +// assert_eq!(history_match.0, FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )); +// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present"); +// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); +// }); + +// let second_query = "fsdasdsa"; +// let finder = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// }); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(second_query.to_string(), cx) +// }) +// .await; +// finder.update(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "No history entries should match {second_query}" +// ); +// assert!( +// delegate.matches.search.is_empty(), +// "No search entries should match {second_query}" +// ); +// }); + +// let first_query_again = first_query; + +// let finder = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// }); +// finder +// .update(cx, |finder, cx| { +// finder +// .delegate +// .update_matches(first_query_again.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query"); +// let history_match = delegate.matches.history.first().unwrap(); +// assert!(history_match.1.is_some(), "Should have path matches for history items after querying"); +// assert_eq!(history_match.0, FoundPath::new( +// ProjectPath { +// worktree_id, +// path: Arc::from(Path::new("test/first.rs")), +// }, +// Some(PathBuf::from("/src/test/first.rs")) +// )); +// assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query"); +// assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs")); +// }); +// } + +// #[gpui::test] +// async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "collab_ui": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// "collab_ui.rs": "// Fourth Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let query = "collab_ui"; +// let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(query.to_string(), cx) +// }) +// .await; +// finder.read_with(cx, |finder, _| { +// let delegate = &finder.delegate; +// assert!( +// delegate.matches.history.is_empty(), +// "History items should not math query {query}, they should be matched by name only" +// ); + +// let search_entries = delegate +// .matches +// .search +// .iter() +// .map(|path_match| path_match.path.to_path_buf()) +// .collect::>(); +// assert_eq!( +// search_entries, +// vec![ +// PathBuf::from("collab_ui/collab_ui.rs"), +// PathBuf::from("collab_ui/third.rs"), +// PathBuf::from("collab_ui/first.rs"), +// PathBuf::from("collab_ui/second.rs"), +// ], +// "Despite all search results having the same directory name, the most matching one should be on top" +// ); +// }); +// } + +// #[gpui::test] +// async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) { +// let app_state = init_test(cx); + +// app_state +// .fs +// .as_fake() +// .insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "nonexistent.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// let cx = &mut cx; +// // generate some history to select from +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; +// open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await; +// open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; +// open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + +// cx.dispatch_action(Toggle); +// let query = "rs"; +// let finder = cx.read(|cx| workspace.read(cx).current_modal::().unwrap()); +// finder +// .update(cx, |finder, cx| { +// finder.picker.update(cx, |picker, cx| { +// picker.delegate.update_matches(query.to_string(), cx) +// }) +// }) +// .await; +// finder.update(cx, |finder, _| { +// let history_entries = finder.delegate +// .matches +// .history +// .iter() +// .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf()) +// .collect::>(); +// assert_eq!( +// history_entries, +// vec![ +// PathBuf::from("test/first.rs"), +// PathBuf::from("test/third.rs"), +// ], +// "Should have all opened files in the history, except the ones that do not exist on disk" +// ); +// }); +// } + +// async fn open_close_queried_buffer( +// input: &str, +// expected_matches: usize, +// expected_editor_title: &str, +// workspace: &View, +// cx: &mut gpui::VisualTestContext<'_>, +// ) -> Vec { +// cx.dispatch_action(Toggle); +// let picker = workspace.update(cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .clone() +// }); +// picker +// .update(cx, |finder, cx| { +// finder.delegate.update_matches(input.to_string(), cx) +// }) +// .await; +// let history_items = picker.update(cx, |finder, _| { +// assert_eq!( +// finder.delegate.matches.len(), +// expected_matches, +// "Unexpected number of matches found for query {input}" +// ); +// finder.delegate.history_items.clone() +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// cx.dispatch_action(SelectNext); +// cx.dispatch_action(Confirm); +// cx.background_executor.run_until_parked(); +// active_pane +// .condition(cx, |pane, _| pane.active_item().is_some()) +// .await; +// cx.read(|cx| { +// let active_item = active_pane.read(cx).active_item().unwrap(); +// let active_editor_title = active_item +// .to_any() +// .downcast::() +// .unwrap() +// .read(cx) +// .title(cx); +// assert_eq!( +// expected_editor_title, active_editor_title, +// "Unexpected editor title for query {input}" +// ); +// }); + +// close_active_item(workspace, cx).await; + +// history_items +// } + +// async fn close_active_item(workspace: &View, cx: &mut VisualTestContext<'_>) { +// let mut original_items = HashMap::new(); +// cx.read(|cx| { +// for pane in workspace.read(cx).panes() { +// let pane_id = pane.entity_id(); +// let pane = pane.read(cx); +// let insertion_result = original_items.insert(pane_id, pane.items().count()); +// assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); +// } +// }); + +// let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); +// active_pane +// .update(cx, |pane, cx| { +// pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx) +// .unwrap() +// }) +// .await +// .unwrap(); +// cx.background_executor.run_until_parked(); +// cx.read(|cx| { +// for pane in workspace.read(cx).panes() { +// let pane_id = pane.entity_id(); +// let pane = pane.read(cx); +// match original_items.remove(&pane_id) { +// Some(original_items) => { +// assert_eq!( +// pane.items().count(), +// original_items.saturating_sub(1), +// "Pane id {pane_id} should have item closed" +// ); +// } +// None => panic!("Pane id {pane_id} not found in original items"), +// } +// } +// }); +// assert!( +// original_items.len() <= 1, +// "At most one panel should got closed" +// ); +// } + +// fn init_test(cx: &mut TestAppContext) -> Arc { +// cx.update(|cx| { +// let state = AppState::test(cx); +// theme::init(cx); +// language::init(cx); +// super::init(cx); +// editor::init(cx); +// workspace::init_settings(cx); +// Project::init_settings(cx); +// state +// }) +// } + +// fn test_path_like(test_str: &str) -> PathLikeWithPosition { +// PathLikeWithPosition::parse_str(test_str, |path_like_str| { +// Ok::<_, std::convert::Infallible>(FileSearchQuery { +// raw_query: test_str.to_owned(), +// file_query_end: if path_like_str == test_str { +// None +// } else { +// Some(path_like_str.len()) +// }, +// }) +// }) +// .unwrap() +// } + +// fn dummy_found_path(project_path: ProjectPath) -> FoundPath { +// FoundPath { +// project: project_path, +// absolute: None, +// } +// } + +// fn build_find_picker( +// project: Model, +// cx: &mut TestAppContext, +// ) -> ( +// View>, +// View, +// VisualTestContext, +// ) { +// let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx)); +// cx.dispatch_action(Toggle); +// let picker = workspace.update(&mut cx, |workspace, cx| { +// workspace +// .current_modal::(cx) +// .unwrap() +// .read(cx) +// .picker +// .clone() +// }); +// (picker, workspace, cx) +// } +// } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 1d57be6fd0b73d91533a446843450f349423f1c1..ccd6b7ada2141ef0cf0d02ba3bbae34cc6a926f2 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,11 +1,11 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, - StatelessInteractive, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString, + Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Label, LabelColor, StyledExt}; +use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::{Modal, ModalEvent, Workspace}; @@ -150,7 +150,7 @@ impl Render for GoToLine { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .elevation_2(cx) - .context("GoToLine") + .key_context("GoToLine") .on_action(Self::cancel) .on_action(Self::confirm) .w_96() @@ -176,7 +176,7 @@ impl Render for GoToLine { .justify_between() .px_2() .py_1() - .child(Label::new(self.current_text.clone()).color(LabelColor::Muted)), + .child(Label::new(self.current_text.clone()).color(TextColor::Muted)), ), ) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4b6b9bea738c393f32e66d5fb84f670dce3fa001..b732be745525ebab56e5b823c852e77876f07de6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2112,6 +2112,10 @@ impl AppContext { AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap()) } + pub fn open_url(&self, url: &str) { + self.platform.open_url(url) + } + pub fn write_to_clipboard(&self, item: ClipboardItem) { self.platform.write_to_clipboard(item); } diff --git a/crates/gpui2/docs/contexts.md b/crates/gpui2/docs/contexts.md new file mode 100644 index 0000000000000000000000000000000000000000..763a902517050da97757969977298180d5850742 --- /dev/null +++ b/crates/gpui2/docs/contexts.md @@ -0,0 +1,41 @@ +# Contexts + +GPUI makes extensive use of *context parameters*, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services. + +There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead. + +``` + AppContext + / \ +ModelContext WindowContext + / + ViewContext +``` + +- The `AppContext` forms the root of the hierarchy +- `ModelContext` and `WindowContext` both dereference to `AppContext` +- `ViewContext` dereferences to `WindowContext` + +## `AppContext` + +Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`... + +## `WindowContext` + +Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`. + +## `ModelContext` + +Available when you create or update a `Model`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events. + +## `ViewContext` + +Available when you create or update a `View`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events. + +## `AsyncAppContext` and `AsyncWindowContext` + +Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself. + +## `TestAppContext` and `TestVisualContext` + +These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests. diff --git a/crates/gpui2/docs/key_dispatch.md b/crates/gpui2/docs/key_dispatch.md new file mode 100644 index 0000000000000000000000000000000000000000..339eb430281b5863b68bda2bd3f71117963adf8e --- /dev/null +++ b/crates/gpui2/docs/key_dispatch.md @@ -0,0 +1,101 @@ +# Key Dispatch + +GPUI is designed for keyboard-first interactivity. + +To expose functionality to the mouse, you render a button with a click handler. + +To expose functionality to the keyboard, you bind an *action* in a *key context*. + +Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself: + +```rust +mod menu { + #[gpui::action] + struct MoveUp; + + #[gpui::action] + struct MoveDown; +} +``` + +Actions are frequently unit structs, for which we have a macro. The above could also be written: + +```rust +mod menu { + actions!(MoveUp, MoveDown); +} +``` + +Actions can also be more complex types: + +```rust +mod menu { + #[gpui::action] + struct Move { + direction: Direction, + select: bool, + } +} +``` + +To bind actions, chain `on_action` on to your element: + +```rust +impl Render for Menu { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { + div() + .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext| { + // ... + }) + .on_action(|this, move: &MoveDown, cx| { + // ... + }) + .children(todo!()) + } +} +``` + +In order to bind keys to actions, you need to declare a *key context* for part of the element tree by calling `key_context`. + +```rust +impl Render for Menu { + fn render(&mut self, cx: &mut ViewContext) -> impl Component { + div() + .key_context("menu") + .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext| { + // ... + }) + .on_action(|this, move: &MoveDown, cx| { + // ... + }) + .children(todo!()) + } +} +``` + +Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name. + +```json +{ + "context": "menu", + "bindings": { + "up": "menu::MoveUp", + "down": "menu::MoveDown" + } +} +``` + +If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name: + +```json +{ + "context": "menu", + "bindings": { + "up": ["menu::Move", {direction: "up", select: false}] + "down": ["menu::Move", {direction: "down", select: false}] + "shift-up": ["menu::Move", {direction: "up", select: true}] + "shift-down": ["menu::Move", {direction: "down", select: true}] + } +} + +``` diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index b6cb3f6307b55d38d2fa7fc12fb346447b8b8cfd..c76b62b510c44fd18e714313777d94ccd9602051 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -234,10 +234,10 @@ impl AppContext { app_version: platform.app_version().ok(), }; - Rc::new_cyclic(|this| AppCell { + let app = Rc::new_cyclic(|this| AppCell { app: RefCell::new(AppContext { this: this.clone(), - platform, + platform: platform.clone(), app_metadata, text_system, flushing_effects: false, @@ -269,12 +269,21 @@ impl AppContext { layout_id_buffer: Default::default(), propagate_event: true, }), - }) + }); + + platform.on_quit(Box::new({ + let cx = app.clone(); + move || { + cx.borrow_mut().shutdown(); + } + })); + + app } /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit` /// will be given 100ms to complete before exiting. - pub fn quit(&mut self) { + pub fn shutdown(&mut self) { let mut futures = Vec::new(); for observer in self.quit_observers.remove(&()) { @@ -292,8 +301,10 @@ impl AppContext { { log::error!("timed out waiting on app_will_quit"); } + } - self.globals_by_type.clear(); + pub fn quit(&mut self) { + self.platform.quit(); } pub fn app_metadata(&self) -> AppMetadata { @@ -431,6 +442,18 @@ impl AppContext { self.platform.activate(ignoring_other_apps); } + pub fn hide(&self) { + self.platform.hide(); + } + + pub fn hide_other_apps(&self) { + self.platform.hide_other_apps(); + } + + pub fn unhide_other_apps(&self) { + self.platform.unhide_other_apps(); + } + /// Returns the list of currently active displays. pub fn displays(&self) -> Vec> { self.platform.displays() @@ -1091,7 +1114,7 @@ impl DerefMut for GlobalLease { /// Contains state associated with an active drag operation, started by dragging an element /// within the window or by dragging into the app from the underlying platform. -pub(crate) struct AnyDrag { +pub struct AnyDrag { pub view: AnyView, pub cursor_offset: Point, } diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index 1ae9aec9b516175c008ff0952a1eba71e27f3652..1e01921cd458caf4ac3795c4d99ebc46a106b768 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -26,7 +26,7 @@ impl EntityId { impl Display for EntityId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self) + write!(f, "{}", self.as_u64()) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 44c31bbd69dad1325da8ec062551c3c03d67d7e2..cc59b7a16a52804c89b4b4b9489fe4ec7945a272 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,8 @@ use crate::{ - div, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, - Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, - ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext, - VisualContext, WindowContext, WindowHandle, WindowOptions, + div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, + BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, + Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, + ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -14,6 +14,7 @@ pub struct TestAppContext { pub background_executor: BackgroundExecutor, pub foreground_executor: ForegroundExecutor, pub dispatcher: TestDispatcher, + pub test_platform: Rc, } impl Context for TestAppContext { @@ -77,17 +78,16 @@ impl TestAppContext { let arc_dispatcher = Arc::new(dispatcher.clone()); let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); let foreground_executor = ForegroundExecutor::new(arc_dispatcher); - let platform = Rc::new(TestPlatform::new( - background_executor.clone(), - foreground_executor.clone(), - )); + let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone()); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); + Self { - app: AppContext::new(platform, asset_source, http_client), + app: AppContext::new(platform.clone(), asset_source, http_client), background_executor, foreground_executor, dispatcher: dispatcher.clone(), + test_platform: platform, } } @@ -96,7 +96,7 @@ impl TestAppContext { } pub fn quit(&self) { - self.app.borrow_mut().quit(); + self.app.borrow_mut().shutdown(); } pub fn refresh(&mut self) -> Result<()> { @@ -152,6 +152,21 @@ impl TestAppContext { (view, VisualTestContext::from_window(*window.deref(), self)) } + pub fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + self.test_platform.simulate_new_path_selection(select_path); + } + + pub fn simulate_prompt_answer(&self, button_ix: usize) { + self.test_platform.simulate_prompt_answer(button_ix); + } + + pub fn has_pending_prompt(&self) -> bool { + self.test_platform.has_pending_prompt() + } + pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, @@ -199,6 +214,15 @@ impl TestAppContext { } } + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) + where + A: Action, + { + window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .unwrap() + } + pub fn dispatch_keystroke( &mut self, window: AnyWindowHandle, @@ -376,6 +400,13 @@ impl<'a> VisualTestContext<'a> { pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { Self { cx, window } } + + pub fn dispatch_action(&mut self, action: A) + where + A: Action, + { + self.cx.dispatch_action(self.window, action) + } } impl<'a> Context for VisualTestContext<'a> { diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index 5f6308ec4fb2d2d9d7efc27eacbb42bf08109a6d..44a0e917be279999c327d112eddad59cc55d8dcc 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -293,7 +293,16 @@ pub fn blue() -> Hsla { pub fn green() -> Hsla { Hsla { - h: 0.3, + h: 0.33, + s: 1., + l: 0.5, + a: 1., + } +} + +pub fn yellow() -> Hsla { + Hsla { + h: 0.16, s: 1., l: 0.5, a: 1., diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 9ee9eaa7c335960f3e3c6974b0a8798c3d13f9c4..46ea5c6cd21b5a297b65c4de1b783b68435d7296 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -8,7 +8,7 @@ use std::{any::Any, mem}; pub trait Element { type ElementState: 'static; - fn id(&self) -> Option; + fn element_id(&self) -> Option; /// Called to initialize this element for the current frame. If this /// element had state in a previous frame, it will be passed in for the 3rd argument. @@ -38,7 +38,7 @@ pub trait Element { #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] pub struct GlobalElementId(SmallVec<[ElementId; 32]>); -pub trait ParentElement { +pub trait ParentComponent { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; fn child(mut self, child: impl Component) -> Self @@ -120,7 +120,7 @@ where E::ElementState: 'static, { fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext) { - let frame_state = if let Some(id) = self.element.id() { + let frame_state = if let Some(id) = self.element.element_id() { cx.with_element_state(id, |element_state, cx| { let element_state = self.element.initialize(view_state, element_state, cx); ((), element_state) @@ -142,7 +142,7 @@ where frame_state: initial_frame_state, } => { frame_state = initial_frame_state; - if let Some(id) = self.element.id() { + if let Some(id) = self.element.element_id() { layout_id = cx.with_element_state(id, |element_state, cx| { let mut element_state = element_state.unwrap(); let layout_id = self.element.layout(state, &mut element_state, cx); @@ -181,7 +181,7 @@ where .. } => { let bounds = cx.layout_bounds(layout_id); - if let Some(id) = self.element.id() { + 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 @@ -255,7 +255,7 @@ where // Ignore the element offset when drawing this element, as the origin is already specified // in absolute terms. origin -= cx.element_offset(); - cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx)) + cx.with_element_offset(origin, |cx| self.paint(view_state, cx)) } } @@ -351,7 +351,7 @@ where { type ElementState = AnyElement; - fn id(&self) -> Option { + fn element_id(&self) -> Option { None } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 95c44038ed115ead27a00d2dcd7a10e1fae429dc..a08d1619ae281c1895afe058fdd4da5cc35aa7d8 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,225 +1,606 @@ -use std::fmt::Debug; - use crate::{ - point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, ElementInteractivity, - FocusHandle, FocusListeners, Focusable, FocusableKeyDispatch, GlobalElementId, GroupBounds, - InteractiveElementState, KeyContext, KeyDispatch, LayoutId, NonFocusableKeyDispatch, Overflow, - ParentElement, Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, - StatelessInteractive, StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, - Visibility, + 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, }; +use collections::HashMap; +use parking_lot::Mutex; use refineable::Refineable; use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + fmt::Debug, + marker::PhantomData, + mem, + sync::Arc, + time::Duration, +}; +use taffy::style::Overflow; use util::ResultExt; -pub struct Div< - V: 'static, - I: ElementInteractivity = StatelessInteractivity, - K: KeyDispatch = NonFocusableKeyDispatch, -> { - interactivity: I, - key_dispatch: K, - children: SmallVec<[AnyElement; 2]>, - group: Option, - base_style: StyleRefinement, +const DRAG_THRESHOLD: f64 = 2.; +const TOOLTIP_DELAY: Duration = Duration::from_millis(500); +const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); + +pub struct GroupStyle { + pub group: SharedString, + pub style: StyleRefinement, } -pub fn div() -> Div, NonFocusableKeyDispatch> { - Div { - interactivity: StatelessInteractivity::default(), - key_dispatch: NonFocusableKeyDispatch::default(), - children: SmallVec::new(), - group: None, - base_style: StyleRefinement::default(), +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 } -} -impl Div, F> -where - V: 'static, - F: KeyDispatch, -{ - pub fn id(self, id: impl Into) -> Div, F> { - Div { - interactivity: StatefulInteractivity::new(id.into(), self.interactivity), - key_dispatch: self.key_dispatch, - children: self.children, - group: self.group, - base_style: self.base_style, + fn id(mut self, id: impl Into) -> Stateful { + self.interactivity().element_id = Some(id.into()); + + Stateful { + element: self, + view_type: PhantomData, + } + } + + 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, } } + + fn key_context(mut self, key_context: C) -> Self + where + C: TryInto, + E: Debug, + { + if let Some(key_context) = key_context.try_into().log_err() { + self.interactivity().key_context = key_context; + } + self + } + + fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { + self.interactivity().hover_style = f(StyleRefinement::default()); + self + } + + fn group_hover( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self { + self.interactivity().group_hover_style = Some(GroupStyle { + group: group_name.into(), + style: f(StyleRefinement::default()), + }); + self + } + + fn on_mouse_down( + mut self, + button: MouseButton, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().mouse_down_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && bounds.contains_point(&event.position) + { + handler(view, event, cx) + } + }, + )); + self + } + + fn on_any_mouse_down( + mut self, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().mouse_down_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx) + } + }, + )); + self + } + + fn on_mouse_up( + mut self, + button: MouseButton, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().mouse_up_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble + && event.button == button + && bounds.contains_point(&event.position) + { + handler(view, 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| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx) + } + }, + )); + self + } + + fn on_mouse_down_out( + mut self, + handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().mouse_down_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { + handler(view, event, cx) + } + }, + )); + self + } + + fn on_mouse_up_out( + mut self, + button: MouseButton, + handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().mouse_up_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Capture + && event.button == button + && !bounds.contains_point(&event.position) + { + handler(view, event, cx); + } + }, + )); + self + } + + fn on_mouse_move( + mut self, + handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().mouse_move_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, event, cx); + } + }, + )); + self + } + + fn on_scroll_wheel( + mut self, + handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().scroll_wheel_listeners.push(Box::new( + move |view, event, bounds, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + handler(view, 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 { + self.interactivity().action_listeners.push(( + TypeId::of::(), + Box::new(move |view, action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Capture { + listener(view, action, cx) + } + }), + )); + self + } + + /// 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 { + self.interactivity().action_listeners.push(( + TypeId::of::(), + Box::new(move |view, action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + listener(view, action, cx) + } + }), + )); + self + } + + fn on_key_down( + mut self, + listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity() + .key_down_listeners + .push(Box::new(move |view, event, phase, cx| { + listener(view, event, phase, cx) + })); + self + } + + fn on_key_up( + mut self, + listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity() + .key_up_listeners + .push(Box::new(move |view, event, phase, cx| { + listener(view, event, phase, cx) + })); + self + } + + fn drag_over(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self { + self.interactivity() + .drag_over_styles + .push((TypeId::of::(), f(StyleRefinement::default()))); + self + } + + fn group_drag_over( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self { + self.interactivity().group_drag_over_styles.push(( + TypeId::of::(), + GroupStyle { + group: group_name.into(), + style: f(StyleRefinement::default()), + }, + )); + self + } + + fn on_drop( + mut self, + listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, + ) -> Self { + self.interactivity().drop_listeners.push(( + TypeId::of::(), + Box::new(move |view, dragged_view, cx| { + listener(view, dragged_view.downcast().unwrap(), cx); + }), + )); + self + } } -impl Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ - pub fn group(mut self, group: impl Into) -> Self { - self.group = Some(group.into()); +pub trait StatefulInteractiveComponent>: InteractiveComponent { + fn focusable(mut self) -> Focusable { + self.interactivity().focusable = true; + Focusable { + element: self, + view_type: PhantomData, + } + } + + fn overflow_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); + self + } + + fn overflow_x_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.x = Some(Overflow::Scroll); self } - pub fn z_index(mut self, z_index: u32) -> Self { - self.base_style.z_index = Some(z_index); + fn overflow_y_scroll(mut self) -> Self { + self.interactivity().base_style.overflow.y = Some(Overflow::Scroll); self } - pub fn context(mut self, context: C) -> Self + fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, - C: TryInto, - C::Error: Debug, { - if let Some(context) = context.try_into().log_err() { - *self.key_dispatch.key_context_mut() = context; - } + self.interactivity().active_style = f(StyleRefinement::default()); self } - pub fn overflow_hidden(mut self) -> Self { - self.base_style.overflow.x = Some(Overflow::Hidden); - self.base_style.overflow.y = Some(Overflow::Hidden); + fn group_active( + mut self, + group_name: impl Into, + f: impl FnOnce(StyleRefinement) -> StyleRefinement, + ) -> Self + where + Self: Sized, + { + self.interactivity().group_active_style = Some(GroupStyle { + group: group_name.into(), + style: f(StyleRefinement::default()), + }); self } - pub fn overflow_hidden_x(mut self) -> Self { - self.base_style.overflow.x = Some(Overflow::Hidden); + fn on_click( + mut self, + listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity() + .click_listeners + .push(Box::new(move |view, event, cx| listener(view, event, cx))); self } - pub fn overflow_hidden_y(mut self) -> Self { - self.base_style.overflow.y = Some(Overflow::Hidden); + fn on_drag( + mut self, + listener: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + ) -> Self + where + Self: Sized, + W: 'static + Render, + { + 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 } - fn with_element_id( - &mut self, - cx: &mut ViewContext, - f: impl FnOnce(&mut Self, Option, &mut ViewContext) -> R, - ) -> R { - if let Some(id) = self.id() { - cx.with_element_id(id, |global_id, cx| f(self, Some(global_id), cx)) - } else { - f(self, None, cx) - } + fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext)) -> Self + where + Self: Sized, + { + 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 } - pub fn compute_style( - &self, - bounds: Bounds, - element_state: &DivState, - cx: &mut ViewContext, - ) -> Style { - let mut computed_style = Style::default(); - computed_style.refine(&self.base_style); - self.key_dispatch.refine_style(&mut computed_style, cx); - self.interactivity.refine_style( - &mut computed_style, - bounds, - &element_state.interactive, - cx, + fn tooltip( + mut self, + build_tooltip: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + ) -> Self + where + Self: Sized, + W: 'static + Render, + { + debug_assert!( + self.interactivity().tooltip_builder.is_none(), + "calling tooltip more than once on the same element is not supported" ); - computed_style + self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| { + build_tooltip(view_state, cx).into() + })); + + self } } -impl Div, NonFocusableKeyDispatch> { - pub fn focusable(self) -> Div, FocusableKeyDispatch> { - Div { - interactivity: self.interactivity, - key_dispatch: FocusableKeyDispatch::new(self.key_dispatch), - children: self.children, - group: self.group, - base_style: self.base_style, - } +pub trait FocusableComponent: InteractiveComponent { + fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().focus_style = f(StyleRefinement::default()); + self } - pub fn track_focus( - self, - handle: &FocusHandle, - ) -> Div, FocusableKeyDispatch> { - Div { - interactivity: self.interactivity, - key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), - children: self.children, - group: self.group, - base_style: self.base_style, - } + fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().focus_in_style = f(StyleRefinement::default()); + self } - pub fn overflow_scroll(mut self) -> Self { - self.base_style.overflow.x = Some(Overflow::Scroll); - self.base_style.overflow.y = Some(Overflow::Scroll); + fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self + where + Self: Sized, + { + self.interactivity().in_focus_style = f(StyleRefinement::default()); self } - pub fn overflow_x_scroll(mut self) -> Self { - self.base_style.overflow.x = Some(Overflow::Scroll); + fn on_focus( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().focus_listeners.push(Box::new( + move |view, focus_handle, event, cx| { + if event.focused.as_ref() == Some(focus_handle) { + listener(view, event, cx) + } + }, + )); self } - pub fn overflow_y_scroll(mut self) -> Self { - self.base_style.overflow.y = Some(Overflow::Scroll); + fn on_blur( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().focus_listeners.push(Box::new( + move |view, focus_handle, event, cx| { + if event.blurred.as_ref() == Some(focus_handle) { + listener(view, event, cx) + } + }, + )); self } -} -impl Div, NonFocusableKeyDispatch> { - pub fn track_focus( - self, - handle: &FocusHandle, - ) -> Div, FocusableKeyDispatch> { - Div { - interactivity: self.interactivity.into_stateful(handle), - key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), - children: self.children, - group: self.group, - base_style: self.base_style, - } + fn on_focus_in( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().focus_listeners.push(Box::new( + move |view, focus_handle, event, cx| { + let descendant_blurred = event + .blurred + .as_ref() + .map_or(false, |blurred| focus_handle.contains(blurred, cx)); + let descendant_focused = event + .focused + .as_ref() + .map_or(false, |focused| focus_handle.contains(focused, cx)); + + if !descendant_blurred && descendant_focused { + listener(view, event, cx) + } + }, + )); + self } -} -impl Focusable for Div> -where - V: 'static, - I: ElementInteractivity, -{ - fn focus_listeners(&mut self) -> &mut FocusListeners { - &mut self.key_dispatch.focus_listeners + fn on_focus_out( + mut self, + listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity().focus_listeners.push(Box::new( + move |view, focus_handle, event, cx| { + let descendant_blurred = event + .blurred + .as_ref() + .map_or(false, |blurred| focus_handle.contains(blurred, cx)); + let descendant_focused = event + .focused + .as_ref() + .map_or(false, |focused| focus_handle.contains(focused, cx)); + if descendant_blurred && !descendant_focused { + listener(view, event, cx) + } + }, + )); + self } +} + +pub type FocusListeners = SmallVec<[FocusListener; 2]>; + +pub type FocusListener = + Box) + 'static>; + +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 MouseMoveListener = Box< + dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, +>; + +pub type ScrollWheelListener = Box< + dyn Fn(&mut V, &ScrollWheelEvent, &Bounds, DispatchPhase, &mut ViewContext) + + 'static, +>; + +pub type ClickListener = Box) + 'static>; + +pub type DragListener = + Box, &mut ViewContext) -> AnyDrag + 'static>; + +type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; + +pub type HoverListener = Box) + 'static>; + +pub type TooltipBuilder = Arc) -> AnyView + 'static>; + +pub type KeyDownListener = + Box) + 'static>; - fn set_focus_style(&mut self, style: StyleRefinement) { - self.key_dispatch.focus_style = style; +pub type KeyUpListener = + Box) + 'static>; + +pub type ActionListener = + Box) + 'static>; + +pub fn div() -> Div { + Div { + interactivity: Interactivity::default(), + children: SmallVec::default(), } +} - fn set_focus_in_style(&mut self, style: StyleRefinement) { - self.key_dispatch.focus_in_style = style; +pub struct Div { + interactivity: Interactivity, + children: SmallVec<[AnyElement; 2]>, +} + +impl Styled for Div { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.interactivity.base_style } +} - fn set_in_focus_style(&mut self, style: StyleRefinement) { - self.key_dispatch.in_focus_style = style; +impl InteractiveComponent for Div { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity } } -#[derive(Default)] -pub struct DivState { - interactive: InteractiveElementState, - focus_handle: Option, - child_layout_ids: SmallVec<[LayoutId; 4]>, +impl ParentComponent for Div { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } } -impl Element for Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Element for Div { type ElementState = DivState; - fn id(&self) -> Option { - self.interactivity - .as_stateful() - .map(|identified| identified.id.clone()) + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() } fn initialize( @@ -228,21 +609,18 @@ where element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - let mut element_state = element_state.unwrap_or_default(); - self.with_element_id(cx, |this, _global_id, cx| { - this.key_dispatch.initialize( - element_state.focus_handle.take(), - cx, - |focus_handle, cx| { - this.interactivity.initialize(cx); - element_state.focus_handle = focus_handle; - for child in &mut this.children { - child.initialize(view_state, cx); - } - }, - ); - }); - element_state + let interactive_state = self + .interactivity + .initialize(element_state.map(|s| s.interactive_state), cx); + + for child in &mut self.children { + child.initialize(view_state, cx); + } + + DivState { + interactive_state, + child_layout_ids: SmallVec::new(), + } } fn layout( @@ -250,19 +628,21 @@ where view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, - ) -> LayoutId { - let style = self.compute_style(Bounds::default(), element_state, cx); - style.apply_text_style(cx, |cx| { - self.with_element_id(cx, |this, _global_id, cx| { - let layout_ids = this - .children - .iter_mut() - .map(|child| child.layout(view_state, cx)) - .collect::>(); - element_state.child_layout_ids = layout_ids.clone(); - cx.request_layout(&style, layout_ids) - }) - }) + ) -> crate::LayoutId { + let mut interactivity = mem::take(&mut self.interactivity); + let layout_id = + interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| { + cx.with_text_style(style.text_style().cloned(), |cx| { + element_state.child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + cx.request_layout(&style, element_state.child_layout_ids.iter().copied()) + }) + }); + self.interactivity = interactivity; + layout_id } fn paint( @@ -272,117 +652,801 @@ where element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - self.with_element_id(cx, |this, _global_id, cx| { - let style = this.compute_style(bounds, element_state, cx); - if style.visibility == Visibility::Hidden { - return; + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + let content_size = if element_state.child_layout_ids.is_empty() { + bounds.size + } else { + for child_layout_id in &element_state.child_layout_ids { + let child_bounds = cx.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.lower_right()); } + (child_max - child_min).into() + }; - if let Some(mouse_cursor) = style.mouse_cursor { - let hovered = bounds.contains_point(&cx.mouse_position()); - if hovered { - cx.set_cursor_style(mouse_cursor); + let mut interactivity = mem::take(&mut self.interactivity); + interactivity.paint( + bounds, + content_size, + &mut element_state.interactive_state, + cx, + |style, scroll_offset, cx| { + if style.visibility == Visibility::Hidden { + return; } - } - if let Some(group) = this.group.clone() { - GroupBounds::push(group, bounds, cx); + let z_index = style.z_index.unwrap_or(0); + + cx.with_z_index(z_index, |cx| { + cx.with_z_index(0, |cx| { + style.paint(bounds, cx); + }); + cx.with_z_index(1, |cx| { + 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 { + child.paint(view_state, cx); + } + }) + }) + }) + }) + }) + }, + ); + self.interactivity = interactivity; + } +} + +impl Component for Div { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +pub struct DivState { + child_layout_ids: SmallVec<[LayoutId; 4]>, + interactive_state: InteractiveElementState, +} + +pub struct Interactivity { + pub element_id: Option, + pub key_context: KeyContext, + pub focusable: bool, + pub tracked_focus_handle: Option, + pub focus_listeners: FocusListeners, + pub group: Option, + pub base_style: StyleRefinement, + pub focus_style: StyleRefinement, + pub focus_in_style: StyleRefinement, + pub in_focus_style: StyleRefinement, + pub hover_style: StyleRefinement, + pub group_hover_style: Option, + pub active_style: StyleRefinement, + 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>, +} + +impl Interactivity +where + V: 'static, +{ + pub fn initialize( + &mut self, + element_state: Option, + cx: &mut ViewContext, + ) -> InteractiveElementState { + let mut element_state = element_state.unwrap_or_default(); + + // Ensure we store a focus handle in our element state if we're focusable. + // If there's an explicit focus handle we're tracking, use that. Otherwise + // create a new handle and store it in the element state, which lives for as + // as frames contain an element with this id. + if self.focusable { + element_state.focus_handle.get_or_insert_with(|| { + self.tracked_focus_handle + .clone() + .unwrap_or_else(|| cx.focus_handle()) + }); + } + + element_state + } + + pub fn layout( + &mut self, + element_state: &mut InteractiveElementState, + cx: &mut ViewContext, + f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, + ) -> LayoutId { + let style = self.compute_style(None, element_state, cx); + f(style, cx) + } + + pub fn paint( + &mut self, + bounds: Bounds, + content_size: Size, + element_state: &mut InteractiveElementState, + cx: &mut ViewContext, + f: impl FnOnce(Style, Point, &mut ViewContext), + ) { + let style = self.compute_style(Some(bounds), element_state, cx); + + if let Some(mouse_cursor) = style.mouse_cursor { + let hovered = bounds.contains_point(&cx.mouse_position()); + if hovered { + cx.set_cursor_style(mouse_cursor); } + } + + for listener in self.mouse_down_listeners.drain(..) { + cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { + listener(state, event, &bounds, phase, cx); + }) + } - let z_index = style.z_index.unwrap_or(0); + for listener in self.mouse_up_listeners.drain(..) { + cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { + listener(state, 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); + }) + } - let mut child_min = point(Pixels::MAX, Pixels::MAX); - let mut child_max = Point::default(); + for listener in self.scroll_wheel_listeners.drain(..) { + cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { + listener(state, event, &bounds, phase, cx); + }) + } - let content_size = if element_state.child_layout_ids.is_empty() { - bounds.size - } else { - for child_layout_id in &element_state.child_layout_ids { - let child_bounds = cx.layout_bounds(*child_layout_id); - child_min = child_min.min(&child_bounds.origin); - child_max = child_max.max(&child_bounds.lower_right()); - } - (child_max - child_min).into() - }; - - cx.with_z_index(z_index, |cx| { - cx.with_z_index(0, |cx| { - style.paint(bounds, cx); - this.key_dispatch.paint(bounds, cx); - this.interactivity.paint( - bounds, - content_size, - style.overflow, - &mut element_state.interactive, - cx, - ); + let hover_group_bounds = self + .group_hover_style + .as_ref() + .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); + + 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| { + if phase == DispatchPhase::Capture { + if group_bounds.contains_point(&event.position) != hovered { + cx.notify(); + } + } + }); + } + + if self.hover_style.is_some() + || (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| { + if phase == DispatchPhase::Capture { + if bounds.contains_point(&event.position) != hovered { + cx.notify(); + } + } + }); + } + + 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| { + 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()) + { + for (drop_state_type, listener) in &drop_listeners { + if *drop_state_type == drag_state_type { + let drag = cx + .active_drag + .take() + .expect("checked for type drag state type above"); + listener(view, drag.view.clone(), cx); + cx.notify(); + cx.stop_propagation(); + } + } + } + } + }); + } + + let click_listeners = mem::take(&mut self.click_listeners); + let drag_listener = mem::take(&mut self.drag_listener); + + if !click_listeners.is_empty() || drag_listener.is_some() { + let pending_mouse_down = element_state.pending_mouse_down.clone(); + let mouse_down = pending_mouse_down.lock().clone(); + if let Some(mouse_down) = mouse_down { + 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| { + if cx.active_drag.is_some() { + if phase == DispatchPhase::Capture { + cx.notify(); + } + } else if phase == DispatchPhase::Bubble + && bounds.contains_point(&event.position) + && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD + { + *active_state.lock() = ElementClickedState::default(); + let cursor_offset = event.position - bounds.origin; + let drag = drag_listener(view_state, cursor_offset, cx); + cx.active_drag = Some(drag); + cx.notify(); + cx.stop_propagation(); + } + }); + } + + cx.on_mouse_event(move |view_state, 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); + } + } + *pending_mouse_down.lock() = None; + cx.notify(); }); - cx.with_z_index(1, |cx| { - style.apply_text_style(cx, |cx| { - style.apply_overflow(bounds, cx, |cx| { - let scroll_offset = element_state.interactive.scroll_offset(); - cx.with_element_offset(scroll_offset, |cx| { - for child in &mut this.children { - child.paint(view_state, cx); - } - }); - }) - }) + } else { + cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + *pending_mouse_down.lock() = Some(event.clone()); + cx.notify(); + } }); + } + } + + if let Some(hover_listener) = self.hover_listener.take() { + 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| { + if phase != DispatchPhase::Bubble { + return; + } + let is_hovered = + bounds.contains_point(&event.position) && has_mouse_down.lock().is_none(); + let mut was_hovered = was_hovered.lock(); + + if is_hovered != was_hovered.clone() { + *was_hovered = is_hovered; + drop(was_hovered); + + hover_listener(view_state, is_hovered, cx); + } + }); + } + + if let Some(tooltip_builder) = self.tooltip_builder.take() { + 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| { + if phase != DispatchPhase::Bubble { + return; + } + + let is_hovered = + bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none(); + if !is_hovered { + active_tooltip.lock().take(); + return; + } + + if active_tooltip.lock().is_none() { + let task = cx.spawn({ + let active_tooltip = active_tooltip.clone(); + let tooltip_builder = tooltip_builder.clone(); + + move |view, mut cx| async move { + cx.background_executor().timer(TOOLTIP_DELAY).await; + view.update(&mut cx, move |view_state, cx| { + active_tooltip.lock().replace(ActiveTooltip { + waiting: None, + tooltip: Some(AnyTooltip { + view: tooltip_builder(view_state, cx), + cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET, + }), + }); + cx.notify(); + }) + .ok(); + } + }); + active_tooltip.lock().replace(ActiveTooltip { + waiting: Some(task), + tooltip: None, + }); + } }); - if let Some(group) = this.group.as_ref() { - GroupBounds::pop(group, cx); + if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { + if active_tooltip.tooltip.is_some() { + cx.active_tooltip = active_tooltip.tooltip.clone() + } } - }) + } + + let active_state = element_state.clicked_state.clone(); + if !active_state.lock().is_clicked() { + cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Capture { + *active_state.lock() = ElementClickedState::default(); + cx.notify(); + } + }); + } else { + let active_group_bounds = self + .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| { + if phase == DispatchPhase::Bubble { + let group = active_group_bounds + .map_or(false, |bounds| bounds.contains_point(&down.position)); + let element = bounds.contains_point(&down.position); + if group || element { + *active_state.lock() = ElementClickedState { group, element }; + cx.notify(); + } + } + }); + } + + let overflow = style.overflow; + if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll { + let scroll_offset = element_state + .scroll_offset + .get_or_insert_with(Arc::default) + .clone(); + 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| { + if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { + let mut scroll_offset = scroll_offset.lock(); + let old_scroll_offset = *scroll_offset; + let delta = event.delta.pixel_delta(line_height); + + if overflow.x == Overflow::Scroll { + scroll_offset.x = + (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.)); + } + + if overflow.y == Overflow::Scroll { + scroll_offset.y = + (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.)); + } + + if *scroll_offset != old_scroll_offset { + cx.notify(); + cx.stop_propagation(); + } + } + }); + } + + if let Some(group) = self.group.clone() { + GroupBounds::push(group, bounds, cx); + } + + let scroll_offset = element_state + .scroll_offset + .as_ref() + .map(|scroll_offset| *scroll_offset.lock()); + + cx.with_key_dispatch( + self.key_context.clone(), + 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); + }) + } + + for listener in self.key_up_listeners.drain(..) { + cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { + listener(state, event, phase, cx); + }) + } + + for (action_type, listener) in self.action_listeners.drain(..) { + cx.on_action(action_type, listener) + } + + 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) + }); + } + } + + f(style, scroll_offset.unwrap_or_default(), cx) + }, + ); + + if let Some(group) = self.group.as_ref() { + GroupBounds::pop(group, cx); + } + } + + pub fn compute_style( + &self, + bounds: Option>, + element_state: &mut InteractiveElementState, + cx: &mut ViewContext, + ) -> Style { + let mut style = Style::default(); + style.refine(&self.base_style); + + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + if focus_handle.contains_focused(cx) { + style.refine(&self.focus_in_style); + } + + if focus_handle.within_focused(cx) { + style.refine(&self.in_focus_style); + } + + if focus_handle.is_focused(cx) { + style.refine(&self.focus_style); + } + } + + if let Some(bounds) = bounds { + let mouse_position = cx.mouse_position(); + if let Some(group_hover) = self.group_hover_style.as_ref() { + if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { + if group_bounds.contains_point(&mouse_position) { + style.refine(&group_hover.style); + } + } + } + if bounds.contains_point(&mouse_position) { + style.refine(&self.hover_style); + } + + if let Some(drag) = cx.active_drag.take() { + for (state_type, group_drag_style) in &self.group_drag_over_styles { + if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { + if *state_type == drag.view.entity_type() + && group_bounds.contains_point(&mouse_position) + { + style.refine(&group_drag_style.style); + } + } + } + + for (state_type, drag_over_style) in &self.drag_over_styles { + if *state_type == drag.view.entity_type() + && bounds.contains_point(&mouse_position) + { + style.refine(drag_over_style); + } + } + + cx.active_drag = Some(drag); + } + } + + let clicked_state = element_state.clicked_state.lock(); + if clicked_state.group { + if let Some(group) = self.group_active_style.as_ref() { + style.refine(&group.style) + } + } + + if clicked_state.element { + style.refine(&self.active_style) + } + + style + } +} + +impl Default for Interactivity { + fn default() -> Self { + Self { + element_id: None, + key_context: KeyContext::default(), + focusable: false, + tracked_focus_handle: None, + focus_listeners: SmallVec::default(), + // scroll_offset: Point::default(), + group: None, + base_style: StyleRefinement::default(), + focus_style: StyleRefinement::default(), + focus_in_style: StyleRefinement::default(), + in_focus_style: StyleRefinement::default(), + hover_style: StyleRefinement::default(), + group_hover_style: None, + active_style: StyleRefinement::default(), + group_active_style: None, + drag_over_styles: SmallVec::new(), + group_drag_over_styles: SmallVec::new(), + mouse_down_listeners: SmallVec::new(), + mouse_up_listeners: SmallVec::new(), + mouse_move_listeners: SmallVec::new(), + scroll_wheel_listeners: SmallVec::new(), + key_down_listeners: SmallVec::new(), + key_up_listeners: SmallVec::new(), + action_listeners: SmallVec::new(), + drop_listeners: SmallVec::new(), + click_listeners: SmallVec::new(), + drag_listener: None, + hover_listener: None, + tooltip_builder: None, + } } } -impl Component for Div +#[derive(Default)] +pub struct InteractiveElementState { + pub focus_handle: Option, + pub clicked_state: Arc>, + pub hover_state: Arc>, + pub pending_mouse_down: Arc>>, + pub scroll_offset: Option>>>, + pub active_tooltip: Arc>>, +} + +pub struct ActiveTooltip { + #[allow(unused)] // used to drop the task + waiting: Option>, + tooltip: Option, +} + +/// Whether or not the element or a group that contains it is clicked by the mouse. +#[derive(Copy, Clone, Default, Eq, PartialEq)] +pub struct ElementClickedState { + pub group: bool, + pub element: bool, +} + +impl ElementClickedState { + fn is_clicked(&self) -> bool { + self.group || self.element + } +} + +#[derive(Default)] +pub struct GroupBounds(HashMap; 1]>>); + +impl GroupBounds { + pub fn get(name: &SharedString, cx: &mut AppContext) -> Option> { + cx.default_global::() + .0 + .get(name) + .and_then(|bounds_stack| bounds_stack.last()) + .cloned() + } + + pub fn push(name: SharedString, bounds: Bounds, cx: &mut AppContext) { + cx.default_global::() + .0 + .entry(name) + .or_default() + .push(bounds); + } + + pub fn pop(name: &SharedString, cx: &mut AppContext) { + cx.default_global::().0.get_mut(name).unwrap().pop(); + } +} + +pub struct Focusable { + element: E, + view_type: PhantomData, +} + +impl> FocusableComponent for Focusable {} + +impl InteractiveComponent for Focusable +where + V: 'static, + E: InteractiveComponent, +{ + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() + } +} + +impl> StatefulInteractiveComponent + for Focusable +{ +} + +impl Styled for Focusable +where + V: 'static, + E: Styled, +{ + fn style(&mut self) -> &mut StyleRefinement { + self.element.style() + } +} + +impl Element for Focusable +where + V: 'static, + E: Element, +{ + type ElementState = E::ElementState; + + fn element_id(&self) -> Option { + self.element.element_id() + } + + fn initialize( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + self.element.initialize(view_state, element_state, cx) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> LayoutId { + self.element.layout(view_state, element_state, cx) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + self.element.paint(bounds, view_state, element_state, cx); + } +} + +impl Component for Focusable where - I: ElementInteractivity, - F: KeyDispatch, + V: 'static, + E: 'static + Element, { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl ParentElement for Div +impl ParentComponent for Focusable where - I: ElementInteractivity, - F: KeyDispatch, + V: 'static, + E: ParentComponent, { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children + self.element.children_mut() } } -impl Styled for Div +pub struct Stateful { + element: E, + view_type: PhantomData, +} + +impl Styled for Stateful where - I: ElementInteractivity, - F: KeyDispatch, + V: 'static, + E: Styled, { fn style(&mut self) -> &mut StyleRefinement { - &mut self.base_style + self.element.style() } } -impl StatelessInteractive for Div +impl StatefulInteractiveComponent for Stateful +where + V: 'static, + E: Element, + Self: InteractiveComponent, +{ +} + +impl InteractiveComponent for Stateful where - I: ElementInteractivity, - F: KeyDispatch, + V: 'static, + E: InteractiveComponent, { - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { - self.interactivity.as_stateless_mut() + fn interactivity(&mut self) -> &mut Interactivity { + self.element.interactivity() } } -impl StatefulInteractive for Div, F> +impl> FocusableComponent for Stateful {} + +impl Element for Stateful where - F: KeyDispatch, + V: 'static, + E: Element, { - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { - &mut self.interactivity + type ElementState = E::ElementState; + + fn element_id(&self) -> Option { + self.element.element_id() + } + + fn initialize( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut ViewContext, + ) -> Self::ElementState { + self.element.initialize(view_state, element_state, cx) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) -> LayoutId { + self.element.layout(view_state, element_state, cx) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + self.element.paint(bounds, view_state, element_state, cx) + } +} + +impl Component for Stateful +where + V: 'static, + E: 'static + Element, +{ + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +impl ParentComponent for Stateful +where + V: 'static, + E: ParentComponent, +{ + 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 1ff088c1afc4a2098dc2ba41e37d0ae2ff6eef55..c5c5fb628e31989f6a575a9b26f11f4336d7857d 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,35 +1,28 @@ use crate::{ - div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementId, - ElementInteractivity, FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId, - NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, - StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, + AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent, + InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, + Styled, ViewContext, }; use futures::FutureExt; use util::ResultExt; -pub struct Img< - V: 'static, - I: ElementInteractivity = StatelessInteractivity, - F: KeyDispatch = NonFocusableKeyDispatch, -> { - base: Div, +pub struct Img { + interactivity: Interactivity, uri: Option, grayscale: bool, } -pub fn img() -> Img, NonFocusableKeyDispatch> { +pub fn img() -> Img { Img { - base: div(), + interactivity: Interactivity::default(), uri: None, grayscale: false, } } -impl Img +impl Img where V: 'static, - I: ElementInteractivity, - F: KeyDispatch, { pub fn uri(mut self, uri: impl Into) -> Self { self.uri = Some(uri.into()); @@ -42,145 +35,90 @@ where } } -impl Img, F> -where - F: KeyDispatch, -{ - pub fn id(self, id: impl Into) -> Img, F> { - Img { - base: self.base.id(id), - uri: self.uri, - grayscale: self.grayscale, - } - } -} - -impl Component for Img -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Component for Img { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Img -where - I: ElementInteractivity, - F: KeyDispatch, -{ - type ElementState = DivState; +impl Element for Img { + type ElementState = InteractiveElementState; - fn id(&self) -> Option { - self.base.id() + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() } fn initialize( &mut self, - view_state: &mut V, + _view_state: &mut V, element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - self.base.initialize(view_state, element_state, cx) + self.interactivity.initialize(element_state, cx) } fn layout( &mut self, - view_state: &mut V, + _view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - self.base.layout(view_state, element_state, cx) + self.interactivity.layout(element_state, cx, |style, cx| { + cx.request_layout(&style, None) + }) } fn paint( &mut self, bounds: Bounds, - view: &mut V, + _view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - cx.with_z_index(0, |cx| { - self.base.paint(bounds, view, element_state, cx); - }); - - let style = self.base.compute_style(bounds, element_state, cx); - let corner_radii = style.corner_radii; - - if let Some(uri) = self.uri.clone() { - // eprintln!(">>> image_cache.get({uri}"); - let image_future = cx.image_cache.get(uri.clone()); - // eprintln!("<<< image_cache.get({uri}"); - if let Some(data) = image_future - .clone() - .now_or_never() - .and_then(ResultExt::log_err) - { - let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); - cx.with_z_index(1, |cx| { - cx.paint_image(bounds, corner_radii, data, self.grayscale) - .log_err() - }); - } else { - cx.spawn(|_, mut cx| async move { - if image_future.await.log_err().is_some() { - cx.on_next_frame(|cx| cx.notify()); + self.interactivity.paint( + bounds, + bounds.size, + element_state, + cx, + |style, _scroll_offset, cx| { + let corner_radii = style.corner_radii; + + if let Some(uri) = self.uri.clone() { + // eprintln!(">>> image_cache.get({uri}"); + let image_future = cx.image_cache.get(uri.clone()); + // eprintln!("<<< image_cache.get({uri}"); + if let Some(data) = image_future + .clone() + .now_or_never() + .and_then(ResultExt::log_err) + { + let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); + cx.with_z_index(1, |cx| { + cx.paint_image(bounds, corner_radii, data, self.grayscale) + .log_err() + }); + } else { + cx.spawn(|_, mut cx| async move { + if image_future.await.log_err().is_some() { + cx.on_next_frame(|cx| cx.notify()); + } + }) + .detach() } - }) - .detach() - } - } + } + }, + ) } } -impl Styled for Img -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Styled for Img { fn style(&mut self) -> &mut StyleRefinement { - self.base.style() + &mut self.interactivity.base_style } } -impl StatelessInteractive for Img -where - I: ElementInteractivity, - F: KeyDispatch, -{ - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { - self.base.stateless_interactivity() - } -} - -impl StatefulInteractive for Img, F> -where - F: KeyDispatch, -{ - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { - self.base.stateful_interactivity() - } -} - -impl Focusable for Img> -where - V: 'static, - I: ElementInteractivity, -{ - fn focus_listeners(&mut self) -> &mut FocusListeners { - self.base.focus_listeners() - } - - fn set_focus_style(&mut self, style: StyleRefinement) { - self.base.set_focus_style(style) - } - - fn set_focus_in_style(&mut self, style: StyleRefinement) { - self.base.set_focus_in_style(style) - } - - fn set_in_focus_style(&mut self, style: StyleRefinement) { - self.base.set_in_focus_style(style) +impl InteractiveComponent for Img { + fn interactivity(&mut self) -> &mut Interactivity { + &mut self.interactivity } } diff --git a/crates/gpui2/src/elements.rs b/crates/gpui2/src/elements/mod.rs similarity index 100% rename from crates/gpui2/src/elements.rs rename to crates/gpui2/src/elements/mod.rs diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index bafedb7f2d8469f4c143b59d6f8c743710ebef8e..4b441ad425ee1bc4fe9b860635d8214ace1c21c3 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,157 +1,88 @@ use crate::{ - div, AnyElement, Bounds, Component, Div, DivState, Element, ElementId, ElementInteractivity, - FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId, - NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, - StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, + AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent, + InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, + Styled, ViewContext, }; use util::ResultExt; -pub struct Svg< - V: 'static, - I: ElementInteractivity = StatelessInteractivity, - F: KeyDispatch = NonFocusableKeyDispatch, -> { - base: Div, +pub struct Svg { + interactivity: Interactivity, path: Option, } -pub fn svg() -> Svg, NonFocusableKeyDispatch> { +pub fn svg() -> Svg { Svg { - base: div(), + interactivity: Interactivity::default(), path: None, } } -impl Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Svg { pub fn path(mut self, path: impl Into) -> Self { self.path = Some(path.into()); self } } -impl Svg, F> -where - F: KeyDispatch, -{ - pub fn id(self, id: impl Into) -> Svg, F> { - Svg { - base: self.base.id(id), - path: self.path, - } - } -} - -impl Component for Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Component for Svg { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ - type ElementState = DivState; +impl Element for Svg { + type ElementState = InteractiveElementState; - fn id(&self) -> Option { - self.base.id() + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() } fn initialize( &mut self, - view_state: &mut V, + _view_state: &mut V, element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - self.base.initialize(view_state, element_state, cx) + self.interactivity.initialize(element_state, cx) } fn layout( &mut self, - view_state: &mut V, + _view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - self.base.layout(view_state, element_state, cx) + self.interactivity.layout(element_state, cx, |style, cx| { + cx.request_layout(&style, None) + }) } fn paint( &mut self, bounds: Bounds, - view: &mut V, + _view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) where Self: Sized, { - self.base.paint(bounds, view, element_state, cx); - let color = self - .base - .compute_style(bounds, element_state, cx) - .text - .color; - if let Some((path, color)) = self.path.as_ref().zip(color) { - cx.paint_svg(bounds, path.clone(), color).log_err(); - } + self.interactivity + .paint(bounds, bounds.size, element_state, cx, |style, _, cx| { + if let Some((path, color)) = self.path.as_ref().zip(style.text.color) { + cx.paint_svg(bounds, path.clone(), color).log_err(); + } + }) } } -impl Styled for Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ +impl Styled for Svg { fn style(&mut self) -> &mut StyleRefinement { - self.base.style() - } -} - -impl StatelessInteractive for Svg -where - I: ElementInteractivity, - F: KeyDispatch, -{ - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { - self.base.stateless_interactivity() + &mut self.interactivity.base_style } } -impl StatefulInteractive for Svg, F> -where - V: 'static, - F: KeyDispatch, -{ - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { - self.base.stateful_interactivity() - } -} - -impl Focusable for Svg> -where - I: ElementInteractivity, -{ - fn focus_listeners(&mut self) -> &mut FocusListeners { - self.base.focus_listeners() - } - - fn set_focus_style(&mut self, style: StyleRefinement) { - self.base.set_focus_style(style) - } - - fn set_focus_in_style(&mut self, style: StyleRefinement) { - self.base.set_focus_in_style(style) - } - - fn set_in_focus_style(&mut self, style: StyleRefinement) { - self.base.set_in_focus_style(style) +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 5d91b23bf3b8d83f8064ed75b394645facf18189..93d087833a563b37b557fc1c09c65ce67136e8ad 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -72,7 +72,7 @@ impl Component for Text { impl Element for Text { type ElementState = Arc>>; - fn id(&self) -> Option { + fn element_id(&self) -> Option { None } diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 6687559d1c811349a3f18a5585f2d2d4110eb16e..84cd216275801bfa5ff35f08471f394402235931 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,24 +1,23 @@ use crate::{ point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, - ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, - StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, - StyleRefinement, Styled, ViewContext, + ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, + Point, Size, StyleRefinement, Styled, ViewContext, }; use parking_lot::Mutex; use smallvec::SmallVec; -use std::{cmp, ops::Range, sync::Arc}; +use std::{cmp, mem, ops::Range, sync::Arc}; 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( - id: Id, +pub fn uniform_list( + id: I, item_count: usize, - f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> SmallVec<[C; 64]>, + f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, ) -> UniformList where - Id: Into, + I: Into, V: 'static, C: Component, { @@ -37,7 +36,10 @@ where .map(|component| component.render()) .collect() }), - interactivity: StatefulInteractivity::new(id, StatelessInteractivity::default()), + interactivity: Interactivity { + element_id: Some(id.into()), + ..Default::default() + }, scroll_handle: None, } } @@ -54,7 +56,7 @@ pub struct UniformList { &'a mut ViewContext, ) -> SmallVec<[AnyElement; 64]>, >, - interactivity: StatefulInteractivity, + interactivity: Interactivity, scroll_handle: Option, } @@ -103,7 +105,7 @@ pub struct UniformListState { impl Element for UniformList { type ElementState = UniformListState; - fn id(&self) -> Option { + fn element_id(&self) -> Option { Some(self.id.clone()) } @@ -113,13 +115,18 @@ impl Element for UniformList { element_state: Option, cx: &mut ViewContext, ) -> Self::ElementState { - element_state.unwrap_or_else(|| { + if let Some(mut element_state) = element_state { + element_state.interactive = self + .interactivity + .initialize(Some(element_state.interactive), cx); + element_state + } else { let item_size = self.measure_item(view_state, None, cx); UniformListState { - interactive: InteractiveElementState::default(), + interactive: self.interactivity.initialize(None, cx), item_size, } - }) + } } fn layout( @@ -132,35 +139,44 @@ impl Element for UniformList { let item_size = element_state.item_size; let rem_size = cx.rem_size(); - cx.request_measured_layout( - self.computed_style(), - rem_size, - move |known_dimensions: Size>, available_space: Size| { - let desired_height = item_size.height * max_items; - let width = known_dimensions - .width - .unwrap_or(match available_space.width { - AvailableSpace::Definite(x) => x, - AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width, - }); - let height = match available_space.height { - AvailableSpace::Definite(x) => desired_height.min(x), - AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, - }; - size(width, height) - }, - ) + self.interactivity + .layout(&mut element_state.interactive, cx, |style, cx| { + cx.request_measured_layout( + style, + rem_size, + move |known_dimensions: Size>, + available_space: Size| { + let desired_height = item_size.height * max_items; + let width = known_dimensions + .width + .unwrap_or(match available_space.width { + AvailableSpace::Definite(x) => x, + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + item_size.width + } + }); + let height = match available_space.height { + AvailableSpace::Definite(x) => desired_height.min(x), + AvailableSpace::MinContent | AvailableSpace::MaxContent => { + desired_height + } + }; + size(width, height) + }, + ) + }) } fn paint( &mut self, - bounds: crate::Bounds, + bounds: Bounds, view_state: &mut V, element_state: &mut Self::ElementState, cx: &mut ViewContext, ) { - let style = self.computed_style(); - + let style = + self.interactivity + .compute_style(Some(bounds), &mut element_state.interactive, cx); let border = style.border_widths.to_pixels(cx.rem_size()); let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); @@ -170,74 +186,79 @@ impl Element for UniformList { - point(border.right + padding.right, border.bottom + padding.bottom), ); - cx.with_z_index(style.z_index.unwrap_or(0), |cx| { - style.paint(bounds, cx); + let item_size = element_state.item_size; + let content_size = Size { + width: padded_bounds.size.width, + height: item_size.height * self.item_count, + }; - let content_size; - 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.lock().replace(ScrollHandleState { - item_height, - list_height: padded_bounds.size.height, - scroll_offset: element_state.interactive.track_scroll_offset(), - }); - } - let visible_item_count = if item_height > px(0.) { - (padded_bounds.size.height / item_height).ceil() as usize + 1 - } else { - 0 - }; - let scroll_offset = element_state - .interactive - .scroll_offset() - .map_or((0.0).into(), |offset| offset.y); - let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize; - let visible_range = first_visible_element_ix - ..cmp::min( - first_visible_element_ix + visible_item_count, - self.item_count, - ); + let mut interactivity = mem::take(&mut self.interactivity); + let shared_scroll_offset = element_state + .interactive + .scroll_offset + .get_or_insert_with(Arc::default) + .clone(); - let mut items = (self.render_items)(view_state, visible_range.clone(), cx); + interactivity.paint( + bounds, + content_size, + &mut element_state.interactive, + cx, + |style, scroll_offset, cx| { + let border = style.border_widths.to_pixels(cx.rem_size()); + let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); - content_size = Size { - width: padded_bounds.size.width, - height: item_height * self.item_count, - }; + let padded_bounds = Bounds::from_corners( + bounds.origin + point(border.left + padding.left, border.top + padding.top), + bounds.lower_right() + - point(border.right + padding.right, border.bottom + padding.bottom), + ); - cx.with_z_index(1, |cx| { - for (item, ix) in items.iter_mut().zip(visible_range) { - let item_origin = - padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset); - let available_space = size( - AvailableSpace::Definite(padded_bounds.size.width), - AvailableSpace::Definite(item_height), - ); - item.draw(item_origin, available_space, view_state, cx); - } - }); - } else { - content_size = Size { - width: bounds.size.width, - height: px(0.), - }; - } + cx.with_z_index(style.z_index.unwrap_or(0), |cx| { + style.paint(bounds, cx); - let overflow = point(style.overflow.x, Overflow::Scroll); + 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.lock().replace(ScrollHandleState { + item_height, + list_height: padded_bounds.size.height, + scroll_offset: shared_scroll_offset, + }); + } + let visible_item_count = if item_height > px(0.) { + (padded_bounds.size.height / item_height).ceil() as usize + 1 + } else { + 0 + }; - cx.with_z_index(0, |cx| { - self.interactivity.paint( - bounds, - content_size, - overflow, - &mut element_state.interactive, - cx, - ); - }); - }) + let first_visible_element_ix = + (-scroll_offset.y / item_height).floor() as usize; + let visible_range = first_visible_element_ix + ..cmp::min( + first_visible_element_ix + visible_item_count, + self.item_count, + ); + + let mut 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) { + let item_origin = padded_bounds.origin + + point(px(0.), item_height * ix + scroll_offset.y); + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.draw(item_origin, available_space, view_state, cx); + } + }); + } + }) + }, + ); + self.interactivity = interactivity; } } @@ -275,14 +296,8 @@ impl UniformList { } } -impl StatelessInteractive for UniformList { - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { - self.interactivity.as_stateless_mut() - } -} - -impl StatefulInteractive for UniformList { - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { +impl InteractiveComponent for UniformList { + fn interactivity(&mut self) -> &mut crate::Interactivity { &mut self.interactivity } } diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index b9747edabdf04327fa4c655b63483b448a7724b2..51390d6be28cb84998b1bcb16e9dd81530e71c85 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -156,7 +156,7 @@ pub enum GlobalKey { } pub trait BorrowAppContext { - fn with_text_style(&mut self, style: TextStyleRefinement, f: F) -> R + fn with_text_style(&mut self, style: Option, f: F) -> R where F: FnOnce(&mut Self) -> R; @@ -167,14 +167,18 @@ impl BorrowAppContext for C where C: BorrowMut, { - fn with_text_style(&mut self, style: TextStyleRefinement, f: F) -> R + fn with_text_style(&mut self, style: Option, f: F) -> R where F: FnOnce(&mut Self) -> R, { - self.borrow_mut().push_text_style(style); - let result = f(self); - self.borrow_mut().pop_text_style(); - result + if let Some(style) = style { + self.borrow_mut().push_text_style(style); + let result = f(self); + self.borrow_mut().pop_text_style(); + result + } else { + f(self) + } } fn set_global(&mut self, global: G) { diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 312121c95410fb2c6c99d0070316a753fa5c2442..013ed2ea482a49cdf26f2411b09d8398d01dc506 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,944 +1,9 @@ use crate::{ - div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, Bounds, Component, - DispatchPhase, Div, Element, ElementId, FocusHandle, KeyContext, Keystroke, Modifiers, - Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, Task, View, + div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, }; -use collections::HashMap; -use derive_more::{Deref, DerefMut}; -use parking_lot::Mutex; -use refineable::Refineable; use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - fmt::Debug, - marker::PhantomData, - mem, - ops::Deref, - path::PathBuf, - sync::Arc, - time::Duration, -}; - -const DRAG_THRESHOLD: f64 = 2.; -const TOOLTIP_DELAY: Duration = Duration::from_millis(500); -const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); - -pub trait StatelessInteractive: Element { - fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity; - - fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.stateless_interactivity().hover_style = f(StyleRefinement::default()); - self - } - - fn group_hover( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity().group_hover_style = Some(GroupStyle { - group: group_name.into(), - style: f(StyleRefinement::default()), - }); - self - } - - fn on_mouse_down( - mut self, - button: MouseButton, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_down_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.contains_point(&event.position) - { - handler(view, event, cx) - } - })); - self - } - - fn on_any_mouse_down( - mut self, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_down_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx) - } - })); - self - } - - fn on_any_mouse_up( - mut self, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_up_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx) - } - })); - self - } - - fn on_mouse_up( - mut self, - button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_up_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.contains_point(&event.position) - { - handler(view, event, cx) - } - })); - self - } - - fn on_mouse_down_out( - mut self, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_down_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { - handler(view, event, cx) - } - })); - self - } - - fn on_mouse_up_out( - mut self, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_up_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { - handler(view, event, cx); - } - })); - self - } - - fn on_mouse_move( - mut self, - handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .mouse_move_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx); - } - })); - self - } - - fn on_scroll_wheel( - mut self, - handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .scroll_wheel_listeners - .push(Box::new(move |view, event, bounds, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, 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 - where - Self: Sized, - { - self.stateless_interactivity().action_listeners.push(( - TypeId::of::(), - Box::new(move |view, action, phase, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Capture { - listener(view, action, cx) - } - }), - )); - self - } - - /// 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 - where - Self: Sized, - { - self.stateless_interactivity().action_listeners.push(( - TypeId::of::(), - Box::new(move |view, action, phase, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Bubble { - listener(view, action, cx) - } - }), - )); - self - } - - fn on_key_down( - mut self, - listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .key_down_listeners - .push(Box::new(move |view, event, phase, cx| { - listener(view, event, phase, cx) - })); - self - } - - fn on_key_up( - mut self, - listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .key_up_listeners - .push(Box::new(move |view, event, phase, cx| { - listener(view, event, phase, cx) - })); - self - } - - fn drag_over(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.stateless_interactivity() - .drag_over_styles - .push((TypeId::of::(), f(StyleRefinement::default()))); - self - } - - fn group_drag_over( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity().group_drag_over_styles.push(( - TypeId::of::(), - GroupStyle { - group: group_name.into(), - style: f(StyleRefinement::default()), - }, - )); - self - } - - fn on_drop( - mut self, - listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateless_interactivity().drop_listeners.push(( - TypeId::of::(), - Box::new(move |view, dragged_view, cx| { - listener(view, dragged_view.downcast().unwrap(), cx); - }), - )); - self - } -} - -pub trait StatefulInteractive: StatelessInteractive { - fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity; - - fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.stateful_interactivity().active_style = f(StyleRefinement::default()); - self - } - - fn group_active( - mut self, - group_name: impl Into, - f: impl FnOnce(StyleRefinement) -> StyleRefinement, - ) -> Self - where - Self: Sized, - { - self.stateful_interactivity().group_active_style = Some(GroupStyle { - group: group_name.into(), - style: f(StyleRefinement::default()), - }); - self - } - - fn on_click( - mut self, - listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.stateful_interactivity() - .click_listeners - .push(Box::new(move |view, event, cx| listener(view, event, cx))); - self - } - - fn on_drag( - mut self, - listener: impl Fn(&mut V, &mut ViewContext) -> View + 'static, - ) -> Self - where - Self: Sized, - W: 'static + Render, - { - debug_assert!( - self.stateful_interactivity().drag_listener.is_none(), - "calling on_drag more than once on the same element is not supported" - ); - self.stateful_interactivity().drag_listener = - Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag { - view: listener(view_state, cx).into(), - cursor_offset, - })); - self - } - - fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext)) -> Self - where - Self: Sized, - { - debug_assert!( - self.stateful_interactivity().hover_listener.is_none(), - "calling on_hover more than once on the same element is not supported" - ); - self.stateful_interactivity().hover_listener = Some(Box::new(listener)); - self - } - - fn tooltip( - mut self, - build_tooltip: impl Fn(&mut V, &mut ViewContext) -> View + 'static, - ) -> Self - where - Self: Sized, - W: 'static + Render, - { - debug_assert!( - self.stateful_interactivity().tooltip_builder.is_none(), - "calling tooltip more than once on the same element is not supported" - ); - self.stateful_interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| { - build_tooltip(view_state, cx).into() - })); - - self - } -} - -pub trait ElementInteractivity: 'static { - fn as_stateless(&self) -> &StatelessInteractivity; - fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity; - fn as_stateful(&self) -> Option<&StatefulInteractivity>; - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity>; - - fn refine_style( - &self, - style: &mut Style, - bounds: Bounds, - element_state: &InteractiveElementState, - cx: &mut ViewContext, - ) { - let mouse_position = cx.mouse_position(); - let stateless = self.as_stateless(); - if let Some(group_hover) = stateless.group_hover_style.as_ref() { - if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) { - if group_bounds.contains_point(&mouse_position) { - style.refine(&group_hover.style); - } - } - } - if bounds.contains_point(&mouse_position) { - style.refine(&stateless.hover_style); - } - - if let Some(drag) = cx.active_drag.take() { - for (state_type, group_drag_style) in &self.as_stateless().group_drag_over_styles { - if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { - if *state_type == drag.view.entity_type() - && group_bounds.contains_point(&mouse_position) - { - style.refine(&group_drag_style.style); - } - } - } - - for (state_type, drag_over_style) in &self.as_stateless().drag_over_styles { - if *state_type == drag.view.entity_type() && bounds.contains_point(&mouse_position) - { - style.refine(drag_over_style); - } - } - - cx.active_drag = Some(drag); - } - - if let Some(stateful) = self.as_stateful() { - let active_state = element_state.active_state.lock(); - if active_state.group { - if let Some(group_style) = stateful.group_active_style.as_ref() { - style.refine(&group_style.style); - } - } - if active_state.element { - style.refine(&stateful.active_style); - } - } - } - - fn initialize(&mut self, cx: &mut ViewContext) { - let stateless = self.as_stateless_mut(); - - for listener in stateless.key_down_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { - listener(state, event, phase, cx); - }) - } - - for listener in stateless.key_up_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { - listener(state, event, phase, cx); - }) - } - - for (action_type, listener) in stateless.action_listeners.drain(..) { - cx.on_action(action_type, listener) - } - } - - fn paint( - &mut self, - bounds: Bounds, - content_size: Size, - overflow: Point, - element_state: &mut InteractiveElementState, - cx: &mut ViewContext, - ) { - let stateless = self.as_stateless_mut(); - for listener in stateless.mouse_down_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); - }) - } - - for listener in stateless.mouse_up_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); - }) - } - - for listener in stateless.mouse_move_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); - }) - } - - for listener in stateless.scroll_wheel_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); - }) - } - - let hover_group_bounds = stateless - .group_hover_style - .as_ref() - .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx)); - - 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| { - if phase == DispatchPhase::Capture { - if group_bounds.contains_point(&event.position) != hovered { - cx.notify(); - } - } - }); - } - - if stateless.hover_style.is_some() - || (cx.active_drag.is_some() && !stateless.drag_over_styles.is_empty()) - { - let hovered = bounds.contains_point(&cx.mouse_position()); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { - if phase == DispatchPhase::Capture { - if bounds.contains_point(&event.position) != hovered { - cx.notify(); - } - } - }); - } - - if cx.active_drag.is_some() { - let drop_listeners = mem::take(&mut stateless.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) = - cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) - { - for (drop_state_type, listener) in &drop_listeners { - if *drop_state_type == drag_state_type { - let drag = cx - .active_drag - .take() - .expect("checked for type drag state type above"); - listener(view, drag.view.clone(), cx); - cx.notify(); - cx.stop_propagation(); - } - } - } - } - }); - } - - if let Some(stateful) = self.as_stateful_mut() { - let click_listeners = mem::take(&mut stateful.click_listeners); - let drag_listener = mem::take(&mut stateful.drag_listener); - - if !click_listeners.is_empty() || drag_listener.is_some() { - let pending_mouse_down = element_state.pending_mouse_down.clone(); - let mouse_down = pending_mouse_down.lock().clone(); - if let Some(mouse_down) = mouse_down { - if let Some(drag_listener) = drag_listener { - let active_state = element_state.active_state.clone(); - - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { - if cx.active_drag.is_some() { - if phase == DispatchPhase::Capture { - cx.notify(); - } - } else if phase == DispatchPhase::Bubble - && bounds.contains_point(&event.position) - && (event.position - mouse_down.position).magnitude() - > DRAG_THRESHOLD - { - *active_state.lock() = ActiveState::default(); - let cursor_offset = event.position - bounds.origin; - let drag = drag_listener(view_state, cursor_offset, cx); - cx.active_drag = Some(drag); - cx.notify(); - cx.stop_propagation(); - } - }); - } - - cx.on_mouse_event(move |view_state, 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); - } - } - *pending_mouse_down.lock() = None; - }); - } else { - cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) - { - *pending_mouse_down.lock() = Some(event.clone()); - } - }); - } - } - - if let Some(hover_listener) = stateful.hover_listener.take() { - 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| { - if phase != DispatchPhase::Bubble { - return; - } - let is_hovered = - bounds.contains_point(&event.position) && has_mouse_down.lock().is_none(); - let mut was_hovered = was_hovered.lock(); - - if is_hovered != was_hovered.clone() { - *was_hovered = is_hovered; - drop(was_hovered); - - hover_listener(view_state, is_hovered, cx); - } - }); - } - - if let Some(tooltip_builder) = stateful.tooltip_builder.take() { - 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| { - if phase != DispatchPhase::Bubble { - return; - } - - let is_hovered = bounds.contains_point(&event.position) - && pending_mouse_down.lock().is_none(); - if !is_hovered { - active_tooltip.lock().take(); - return; - } - - if active_tooltip.lock().is_none() { - let task = cx.spawn({ - let active_tooltip = active_tooltip.clone(); - let tooltip_builder = tooltip_builder.clone(); - - move |view, mut cx| async move { - cx.background_executor().timer(TOOLTIP_DELAY).await; - view.update(&mut cx, move |view_state, cx| { - active_tooltip.lock().replace(ActiveTooltip { - waiting: None, - tooltip: Some(AnyTooltip { - view: tooltip_builder(view_state, cx), - cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET, - }), - }); - cx.notify(); - }) - .ok(); - } - }); - active_tooltip.lock().replace(ActiveTooltip { - waiting: Some(task), - tooltip: None, - }); - } - }); - - if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() { - if active_tooltip.tooltip.is_some() { - cx.active_tooltip = active_tooltip.tooltip.clone() - } - } - } - - let active_state = element_state.active_state.clone(); - if active_state.lock().is_none() { - let active_group_bounds = stateful - .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| { - if phase == DispatchPhase::Bubble { - let group = active_group_bounds - .map_or(false, |bounds| bounds.contains_point(&down.position)); - let element = bounds.contains_point(&down.position); - if group || element { - *active_state.lock() = ActiveState { group, element }; - cx.notify(); - } - } - }); - } else { - cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Capture { - *active_state.lock() = ActiveState::default(); - cx.notify(); - } - }); - } - - if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll { - let scroll_offset = element_state - .scroll_offset - .get_or_insert_with(Arc::default) - .clone(); - 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| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - let mut scroll_offset = scroll_offset.lock(); - let old_scroll_offset = *scroll_offset; - let delta = event.delta.pixel_delta(line_height); - - if overflow.x == Overflow::Scroll { - scroll_offset.x = - (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.)); - } - - if overflow.y == Overflow::Scroll { - scroll_offset.y = - (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.)); - } - - if *scroll_offset != old_scroll_offset { - cx.notify(); - cx.stop_propagation(); - } - } - }); - } - } - } -} - -#[derive(Deref, DerefMut)] -pub struct StatefulInteractivity { - pub id: ElementId, - #[deref] - #[deref_mut] - stateless: StatelessInteractivity, - click_listeners: SmallVec<[ClickListener; 2]>, - active_style: StyleRefinement, - group_active_style: Option, - drag_listener: Option>, - hover_listener: Option>, - tooltip_builder: Option>, -} - -impl StatefulInteractivity { - pub fn new(id: ElementId, stateless: StatelessInteractivity) -> Self { - Self { - id, - stateless, - click_listeners: SmallVec::new(), - active_style: StyleRefinement::default(), - group_active_style: None, - drag_listener: None, - hover_listener: None, - tooltip_builder: None, - } - } -} - -impl ElementInteractivity for StatefulInteractivity { - fn as_stateful(&self) -> Option<&StatefulInteractivity> { - Some(self) - } - - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity> { - Some(self) - } - - fn as_stateless(&self) -> &StatelessInteractivity { - &self.stateless - } - - fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity { - &mut self.stateless - } -} - -type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; - -pub struct StatelessInteractivity { - pub dispatch_context: KeyContext, - 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 hover_style: StyleRefinement, - pub group_hover_style: Option, - drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, - group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - drop_listeners: SmallVec<[(TypeId, Box>); 2]>, -} - -impl StatelessInteractivity { - pub fn into_stateful(self, id: impl Into) -> StatefulInteractivity { - StatefulInteractivity { - id: id.into(), - stateless: self, - click_listeners: SmallVec::new(), - drag_listener: None, - hover_listener: None, - tooltip_builder: None, - active_style: StyleRefinement::default(), - group_active_style: None, - } - } -} - -pub struct GroupStyle { - pub group: SharedString, - pub style: StyleRefinement, -} - -#[derive(Default)] -pub struct GroupBounds(HashMap; 1]>>); - -impl GroupBounds { - pub fn get(name: &SharedString, cx: &mut AppContext) -> Option> { - cx.default_global::() - .0 - .get(name) - .and_then(|bounds_stack| bounds_stack.last()) - .cloned() - } - - pub fn push(name: SharedString, bounds: Bounds, cx: &mut AppContext) { - cx.default_global::() - .0 - .entry(name) - .or_default() - .push(bounds); - } - - pub fn pop(name: &SharedString, cx: &mut AppContext) { - cx.default_global::().0.get_mut(name).unwrap().pop(); - } -} - -#[derive(Copy, Clone, Default, Eq, PartialEq)] -struct ActiveState { - pub group: bool, - pub element: bool, -} - -impl ActiveState { - pub fn is_none(&self) -> bool { - !self.group && !self.element - } -} - -#[derive(Default)] -pub struct InteractiveElementState { - active_state: Arc>, - hover_state: Arc>, - pending_mouse_down: Arc>>, - scroll_offset: Option>>>, - active_tooltip: Arc>>, -} - -struct ActiveTooltip { - #[allow(unused)] // used to drop the task - waiting: Option>, - tooltip: Option, -} - -impl InteractiveElementState { - pub fn scroll_offset(&self) -> Option> { - self.scroll_offset - .as_ref() - .map(|offset| offset.lock().clone()) - } - - pub fn track_scroll_offset(&mut self) -> Arc>> { - self.scroll_offset - .get_or_insert_with(|| Arc::new(Mutex::new(Default::default()))) - .clone() - } -} - -impl Default for StatelessInteractivity { - fn default() -> Self { - Self { - dispatch_context: KeyContext::default(), - mouse_down_listeners: SmallVec::new(), - mouse_up_listeners: SmallVec::new(), - mouse_move_listeners: SmallVec::new(), - scroll_wheel_listeners: SmallVec::new(), - key_down_listeners: SmallVec::new(), - key_up_listeners: SmallVec::new(), - action_listeners: SmallVec::new(), - hover_style: StyleRefinement::default(), - group_hover_style: None, - drag_over_styles: SmallVec::new(), - group_drag_over_styles: SmallVec::new(), - drop_listeners: SmallVec::new(), - } - } -} - -impl ElementInteractivity for StatelessInteractivity { - fn as_stateful(&self) -> Option<&StatefulInteractivity> { - None - } - - fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity> { - None - } - - fn as_stateless(&self) -> &StatelessInteractivity { - self - } - - fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity { - self - } -} +use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct KeyDownEvent { @@ -1021,10 +86,6 @@ where } } -// impl Render for Drag { -// // fn render(&mut self, cx: ViewContext) -> -// } - #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] pub enum MouseButton { Left, @@ -1222,45 +283,11 @@ pub struct FocusEvent { pub focused: Option, } -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 MouseMoveListener = Box< - dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; - -pub type ScrollWheelListener = Box< - dyn Fn(&mut V, &ScrollWheelEvent, &Bounds, DispatchPhase, &mut ViewContext) - + 'static, ->; - -pub type ClickListener = Box) + 'static>; - -pub(crate) type DragListener = - Box, &mut ViewContext) -> AnyDrag + 'static>; - -pub(crate) type HoverListener = Box) + 'static>; - -pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; - -pub(crate) type KeyDownListener = - Box) + 'static>; - -pub(crate) type KeyUpListener = - Box) + 'static>; - -pub type ActionListener = - Box) + 'static>; - #[cfg(test)] mod test { use crate::{ - self as gpui, div, Div, FocusHandle, KeyBinding, Keystroke, ParentElement, Render, - StatefulInteractivity, StatelessInteractive, TestAppContext, VisualContext, + self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding, + Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext, }; struct TestView { @@ -1272,15 +299,20 @@ mod test { actions!(TestAction); impl Render for TestView { - type Element = Div>; + type Element = Stateful>; fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { div().id("testview").child( div() - .context("test") - .track_focus(&self.focus_handle) + .key_context("parent") .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) - .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = 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() + }), ) } } @@ -1298,7 +330,7 @@ mod test { }); cx.update(|cx| { - cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, None)]); + cx.bind_keys(vec![KeyBinding::new("ctrl-g", TestAction, Some("parent"))]); }); window diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 323fd7d2ff323c8606142b6182cf829961b15cff..f737c6e30b205d867133bd86561fbcd0dc10a3e6 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,11 +1,9 @@ use crate::{ - build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, - FocusId, KeyBinding, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, - Pixels, Style, StyleRefinement, ViewContext, WindowContext, + build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, + Keymap, Keystroke, KeystrokeMatcher, WindowContext, }; use collections::HashMap; use parking_lot::Mutex; -use refineable::Refineable; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -14,10 +12,6 @@ use std::{ }; use util::ResultExt; -pub type FocusListeners = SmallVec<[FocusListener; 2]>; -pub type FocusListener = - Box) + 'static>; - #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct DispatchNodeId(usize); @@ -208,258 +202,3 @@ impl DispatchTree { *self.node_stack.last().unwrap() } } - -pub trait KeyDispatch: 'static { - fn as_focusable(&self) -> Option<&FocusableKeyDispatch>; - fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch>; - fn key_context(&self) -> &KeyContext; - fn key_context_mut(&mut self) -> &mut KeyContext; - - fn initialize( - &mut self, - focus_handle: Option, - cx: &mut ViewContext, - f: impl FnOnce(Option, &mut ViewContext) -> R, - ) -> R { - let focus_handle = if let Some(focusable) = self.as_focusable_mut() { - let focus_handle = focusable - .focus_handle - .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle())) - .clone(); - for listener in focusable.focus_listeners.drain(..) { - let focus_handle = focus_handle.clone(); - cx.on_focus_changed(move |view, event, cx| { - listener(view, &focus_handle, event, cx) - }); - } - Some(focus_handle) - } else { - None - }; - - cx.with_key_dispatch(self.key_context().clone(), focus_handle, f) - } - - fn refine_style(&self, style: &mut Style, cx: &WindowContext) { - if let Some(focusable) = self.as_focusable() { - let focus_handle = focusable - .focus_handle - .as_ref() - .expect("must call initialize before refine_style"); - if focus_handle.contains_focused(cx) { - style.refine(&focusable.focus_in_style); - } - - if focus_handle.within_focused(cx) { - style.refine(&focusable.in_focus_style); - } - - if focus_handle.is_focused(cx) { - style.refine(&focusable.focus_style); - } - } - } - - fn paint(&self, bounds: Bounds, cx: &mut WindowContext) { - if let Some(focusable) = self.as_focusable() { - let focus_handle = focusable - .focus_handle - .clone() - .expect("must call initialize before paint"); - cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { - if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - if !cx.default_prevented() { - cx.focus(&focus_handle); - cx.prevent_default(); - } - } - }) - } - } -} - -pub struct FocusableKeyDispatch { - pub non_focusable: NonFocusableKeyDispatch, - pub focus_handle: Option, - pub focus_listeners: FocusListeners, - pub focus_style: StyleRefinement, - pub focus_in_style: StyleRefinement, - pub in_focus_style: StyleRefinement, -} - -impl FocusableKeyDispatch { - pub fn new(non_focusable: NonFocusableKeyDispatch) -> Self { - Self { - non_focusable, - focus_handle: None, - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } - - pub fn tracked(non_focusable: NonFocusableKeyDispatch, handle: &FocusHandle) -> Self { - Self { - non_focusable, - focus_handle: Some(handle.clone()), - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } -} - -impl KeyDispatch for FocusableKeyDispatch { - fn as_focusable(&self) -> Option<&FocusableKeyDispatch> { - Some(self) - } - - fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch> { - Some(self) - } - - fn key_context(&self) -> &KeyContext { - &self.non_focusable.key_context - } - - fn key_context_mut(&mut self) -> &mut KeyContext { - &mut self.non_focusable.key_context - } -} - -#[derive(Default)] -pub struct NonFocusableKeyDispatch { - pub(crate) key_context: KeyContext, -} - -impl KeyDispatch for NonFocusableKeyDispatch { - fn as_focusable(&self) -> Option<&FocusableKeyDispatch> { - None - } - - fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch> { - None - } - - fn key_context(&self) -> &KeyContext { - &self.key_context - } - - fn key_context_mut(&mut self) -> &mut KeyContext { - &mut self.key_context - } -} - -pub trait Focusable: Element { - fn focus_listeners(&mut self) -> &mut FocusListeners; - fn set_focus_style(&mut self, style: StyleRefinement); - fn set_focus_in_style(&mut self, style: StyleRefinement); - fn set_in_focus_style(&mut self, style: StyleRefinement); - - fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_focus_style(f(StyleRefinement::default())); - self - } - - fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_focus_in_style(f(StyleRefinement::default())); - self - } - - fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self - where - Self: Sized, - { - self.set_in_focus_style(f(StyleRefinement::default())); - self - } - - fn on_focus( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - if event.focused.as_ref() == Some(focus_handle) { - listener(view, event, cx) - } - })); - self - } - - fn on_blur( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - if event.blurred.as_ref() == Some(focus_handle) { - listener(view, event, cx) - } - })); - self - } - - fn on_focus_in( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - let descendant_blurred = event - .blurred - .as_ref() - .map_or(false, |blurred| focus_handle.contains(blurred, cx)); - let descendant_focused = event - .focused - .as_ref() - .map_or(false, |focused| focus_handle.contains(focused, cx)); - - if !descendant_blurred && descendant_focused { - listener(view, event, cx) - } - })); - self - } - - fn on_focus_out( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.focus_listeners() - .push(Box::new(move |view, focus_handle, event, cx| { - let descendant_blurred = event - .blurred - .as_ref() - .map_or(false, |blurred| focus_handle.contains(blurred, cx)); - let descendant_focused = event - .focused - .as_ref() - .map_or(false, |focused| focus_handle.contains(focused, cx)); - if descendant_blurred && !descendant_focused { - listener(view, event, cx) - } - })); - self - } -} diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index bab9c0f5757354b933849ca7b324061df4812cec..a842913aef5969cf291b92c776fe866378a478b7 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -97,6 +97,7 @@ impl KeystrokeMatcher { } } +#[derive(Debug)] pub enum KeyMatch { None, Pending, diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 8b49addec9cc6caf4f7b06ca8d23223d34535ef5..00ce3340f82b6466990a0bc75cfdea175bad4edb 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync { fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option; fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result>; - fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size, Vec)>; + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + raster_bounds: Bounds, + ) -> Result<(Size, Vec)>; fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; fn wrap_line( &self, diff --git a/crates/gpui2/src/platform/mac/text_system.rs b/crates/gpui2/src/platform/mac/text_system.rs index b87db09dc0ac16bee9e76bcf92e00b21761c1143..155f3097fec2e13b70355b78d9625fd74248bf82 100644 --- a/crates/gpui2/src/platform/mac/text_system.rs +++ b/crates/gpui2/src/platform/mac/text_system.rs @@ -116,7 +116,9 @@ impl PlatformTextSystem for MacTextSystem { }, )?; - Ok(candidates[ix]) + let font_id = candidates[ix]; + lock.font_selections.insert(font.clone(), font_id); + Ok(font_id) } } @@ -145,8 +147,9 @@ impl PlatformTextSystem for MacTextSystem { fn rasterize_glyph( &self, glyph_id: &RenderGlyphParams, + raster_bounds: Bounds, ) -> Result<(Size, Vec)> { - self.0.read().rasterize_glyph(glyph_id) + self.0.read().rasterize_glyph(glyph_id, raster_bounds) } fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { @@ -247,8 +250,11 @@ impl MacTextSystemState { .into()) } - fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size, Vec)> { - let glyph_bounds = self.raster_bounds(params)?; + fn rasterize_glyph( + &self, + params: &RenderGlyphParams, + glyph_bounds: Bounds, + ) -> Result<(Size, Vec)> { if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { Err(anyhow!("glyph bounds are empty")) } else { @@ -260,6 +266,7 @@ impl MacTextSystemState { if params.subpixel_variant.y > 0 { bitmap_size.height += DevicePixels(1); } + let bitmap_size = bitmap_size; let mut bytes; let cx; diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 4afcc4fc1ae436ef0b089809ac5e1b5b86207076..df1ed9b2a6dee715fb707b821e0cdba533ac90b2 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -3,8 +3,15 @@ use crate::{ PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, }; use anyhow::{anyhow, Result}; +use collections::VecDeque; +use futures::channel::oneshot; use parking_lot::Mutex; -use std::{rc::Rc, sync::Arc}; +use std::{ + cell::RefCell, + path::PathBuf, + rc::{Rc, Weak}, + sync::Arc, +}; pub struct TestPlatform { background_executor: BackgroundExecutor, @@ -13,18 +20,60 @@ pub struct TestPlatform { active_window: Arc>>, active_display: Rc, active_cursor: Mutex, + pub(crate) prompts: RefCell, + weak: Weak, +} + +#[derive(Default)] +pub(crate) struct TestPrompts { + multiple_choice: VecDeque>, + new_path: VecDeque<(PathBuf, oneshot::Sender>)>, } impl TestPlatform { - pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self { - TestPlatform { + pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc { + Rc::new_cyclic(|weak| TestPlatform { background_executor: executor, foreground_executor, - + prompts: Default::default(), active_cursor: Default::default(), active_display: Rc::new(TestDisplay::new()), active_window: Default::default(), - } + weak: weak.clone(), + }) + } + + pub(crate) fn simulate_new_path_selection( + &self, + select_path: impl FnOnce(&std::path::Path) -> Option, + ) { + let (path, tx) = self + .prompts + .borrow_mut() + .new_path + .pop_front() + .expect("no pending new path prompt"); + tx.send(select_path(&path)).ok(); + } + + pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) { + let tx = self + .prompts + .borrow_mut() + .multiple_choice + .pop_front() + .expect("no pending multiple choice prompt"); + tx.send(response_ix).ok(); + } + + pub(crate) fn has_pending_prompt(&self) -> bool { + !self.prompts.borrow().multiple_choice.is_empty() + } + + pub(crate) fn prompt(&self) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); + self.prompts.borrow_mut().multiple_choice.push_back(tx); + rx } } @@ -46,9 +95,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn quit(&self) { - unimplemented!() - } + fn quit(&self) {} fn restart(&self) { unimplemented!() @@ -88,7 +135,11 @@ impl Platform for TestPlatform { options: WindowOptions, ) -> Box { *self.active_window.lock() = Some(handle); - Box::new(TestWindow::new(options, self.active_display.clone())) + Box::new(TestWindow::new( + options, + self.weak.clone(), + self.active_display.clone(), + )) } fn set_display_link_output_callback( @@ -118,15 +169,20 @@ impl Platform for TestPlatform { fn prompt_for_paths( &self, _options: crate::PathPromptOptions, - ) -> futures::channel::oneshot::Receiver>> { + ) -> oneshot::Receiver>> { unimplemented!() } fn prompt_for_new_path( &self, - _directory: &std::path::Path, - ) -> futures::channel::oneshot::Receiver> { - unimplemented!() + directory: &std::path::Path, + ) -> oneshot::Receiver> { + let (tx, rx) = oneshot::channel(); + self.prompts + .borrow_mut() + .new_path + .push_back((directory.to_path_buf(), tx)); + rx } fn reveal_path(&self, _path: &std::path::Path) { @@ -141,9 +197,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn on_quit(&self, _callback: Box) { - unimplemented!() - } + fn on_quit(&self, _callback: Box) {} fn on_reopen(&self, _callback: Box) { unimplemented!() diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index cf9143162e46a84f3a330bd2cc81747241674138..adb15c426629bf8f1292fbfd2eed044367408b21 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -1,15 +1,13 @@ -use std::{ - rc::Rc, - sync::{self, Arc}, +use crate::{ + px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, + PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId, + WindowAppearance, WindowBounds, WindowOptions, }; - use collections::HashMap; use parking_lot::Mutex; - -use crate::{ - px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay, - PlatformInputHandler, PlatformWindow, Point, Scene, Size, TileId, WindowAppearance, - WindowBounds, WindowOptions, +use std::{ + rc::{Rc, Weak}, + sync::{self, Arc}, }; #[derive(Default)] @@ -25,16 +23,22 @@ pub struct TestWindow { current_scene: Mutex>, display: Rc, input_handler: Option>, - handlers: Mutex, + platform: Weak, sprite_atlas: Arc, } + impl TestWindow { - pub fn new(options: WindowOptions, display: Rc) -> Self { + pub fn new( + options: WindowOptions, + platform: Weak, + display: Rc, + ) -> Self { Self { bounds: options.bounds, current_scene: Default::default(), display, + platform, input_handler: None, sprite_atlas: Arc::new(TestAtlas::new()), handlers: Default::default(), @@ -89,7 +93,7 @@ impl PlatformWindow for TestWindow { _msg: &str, _answers: &[&str], ) -> futures::channel::oneshot::Receiver { - todo!() + self.platform.upgrade().expect("platform dropped").prompt() } fn activate(&self) { diff --git a/crates/gpui2/src/prelude.rs b/crates/gpui2/src/prelude.rs index bc998fc1f45e144a22fd7f0103db44b27622b884..7c2ad3f07ff8877d83342f1ac60087b9859eacbc 100644 --- a/crates/gpui2/src/prelude.rs +++ b/crates/gpui2/src/prelude.rs @@ -1 +1,4 @@ -pub use crate::{Context, ParentElement, Refineable}; +pub use crate::{ + BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent, + ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext, +}; diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 20972ca84655fcf1f8fe8d1d7137a4a22003787d..35b08b84c2c6b80eb4bfc92d756a90ab5db6e3a9 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,8 +1,8 @@ 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, WindowContext, + FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, + Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, }; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; @@ -220,7 +220,7 @@ pub struct HighlightStyle { impl Eq for HighlightStyle {} impl Style { - pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> { + pub fn text_style(&self) -> Option<&TextStyleRefinement> { if self.text.is_some() { Some(&self.text) } else { @@ -228,13 +228,47 @@ impl Style { } } + pub fn overflow_mask(&self, bounds: Bounds) -> Option> { + match self.overflow { + Point { + x: Overflow::Visible, + y: Overflow::Visible, + } => None, + _ => { + let current_mask = bounds; + let min = current_mask.origin; + let max = current_mask.lower_right(); + let bounds = match ( + self.overflow.x == Overflow::Visible, + self.overflow.y == Overflow::Visible, + ) { + // x and y both visible + (true, true) => return None, + // x visible, y hidden + (true, false) => Bounds::from_corners( + point(min.x, bounds.origin.y), + point(max.x, bounds.lower_right().y), + ), + // x hidden, y visible + (false, true) => Bounds::from_corners( + point(bounds.origin.x, min.y), + point(bounds.lower_right().x, max.y), + ), + // both hidden + (false, false) => bounds, + }; + Some(ContentMask { bounds }) + } + } + } + pub fn apply_text_style(&self, cx: &mut C, f: F) -> R where C: BorrowAppContext, F: FnOnce(&mut C) -> R, { if self.text.is_some() { - cx.with_text_style(self.text.clone(), f) + cx.with_text_style(Some(self.text.clone()), f) } else { f(cx) } @@ -274,7 +308,7 @@ impl Style { bounds: mask_bounds, }; - cx.with_content_mask(mask, f) + cx.with_content_mask(Some(mask), f) } /// Paints the background of an element styled with this style. diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index 3f1fa843f99abb689ca609940dd2cbc55fb16dbd..beaf664dd8a51fcd58eba73feb247992945f0217 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -1,26 +1,24 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, - SharedString, Style, StyleRefinement, Visibility, + SharedString, StyleRefinement, Visibility, }; use crate::{BoxShadow, TextStyleRefinement}; -use refineable::Refineable; use smallvec::{smallvec, SmallVec}; +use taffy::style::Overflow; -pub trait Styled { +pub trait Styled: Sized { fn style(&mut self) -> &mut StyleRefinement; - fn computed_style(&mut self) -> Style { - Style::default().refined(self.style().clone()) - } - gpui2_macros::style_helpers!(); + fn z_index(mut self, z_index: u32) -> Self { + self.style().z_index = Some(z_index); + self + } + /// Sets the size of the element to the full width and height. - fn full(mut self) -> Self - where - Self: Sized, - { + fn full(mut self) -> Self { self.style().size.width = Some(relative(1.).into()); self.style().size.height = Some(relative(1.).into()); self @@ -28,118 +26,98 @@ pub trait Styled { /// Sets the position of the element to `relative`. /// [Docs](https://tailwindcss.com/docs/position) - fn relative(mut self) -> Self - where - Self: Sized, - { + fn relative(mut self) -> Self { self.style().position = Some(Position::Relative); self } /// Sets the position of the element to `absolute`. /// [Docs](https://tailwindcss.com/docs/position) - fn absolute(mut self) -> Self - where - Self: Sized, - { + fn absolute(mut self) -> Self { self.style().position = Some(Position::Absolute); self } /// Sets the display type of the element to `block`. /// [Docs](https://tailwindcss.com/docs/display) - fn block(mut self) -> Self - where - Self: Sized, - { + fn block(mut self) -> Self { self.style().display = Some(Display::Block); self } /// Sets the display type of the element to `flex`. /// [Docs](https://tailwindcss.com/docs/display) - fn flex(mut self) -> Self - where - Self: Sized, - { + fn flex(mut self) -> Self { self.style().display = Some(Display::Flex); self } /// Sets the visibility of the element to `visible`. /// [Docs](https://tailwindcss.com/docs/visibility) - fn visible(mut self) -> Self - where - Self: Sized, - { + fn visible(mut self) -> Self { self.style().visibility = Some(Visibility::Visible); self } /// Sets the visibility of the element to `hidden`. /// [Docs](https://tailwindcss.com/docs/visibility) - fn invisible(mut self) -> Self - where - Self: Sized, - { + fn invisible(mut self) -> Self { self.style().visibility = Some(Visibility::Hidden); self } - fn cursor(mut self, cursor: CursorStyle) -> Self - where - Self: Sized, - { + fn overflow_hidden(mut self) -> Self { + self.style().overflow.x = Some(Overflow::Hidden); + self.style().overflow.y = Some(Overflow::Hidden); + self + } + + fn overflow_hidden_x(mut self) -> Self { + self.style().overflow.x = Some(Overflow::Hidden); + self + } + + fn overflow_hidden_y(mut self) -> Self { + self.style().overflow.y = Some(Overflow::Hidden); + self + } + + fn cursor(mut self, cursor: CursorStyle) -> Self { self.style().mouse_cursor = Some(cursor); self } /// Sets the cursor style when hovering an element to `default`. /// [Docs](https://tailwindcss.com/docs/cursor) - fn cursor_default(mut self) -> Self - where - Self: Sized, - { + fn cursor_default(mut self) -> Self { self.style().mouse_cursor = Some(CursorStyle::Arrow); self } /// Sets the cursor style when hovering an element to `pointer`. /// [Docs](https://tailwindcss.com/docs/cursor) - fn cursor_pointer(mut self) -> Self - where - Self: Sized, - { + fn cursor_pointer(mut self) -> Self { self.style().mouse_cursor = Some(CursorStyle::PointingHand); self } /// Sets the flex direction of the element to `column`. /// [Docs](https://tailwindcss.com/docs/flex-direction#column) - fn flex_col(mut self) -> Self - where - Self: Sized, - { + fn flex_col(mut self) -> Self { self.style().flex_direction = Some(FlexDirection::Column); self } /// Sets the flex direction of the element to `row`. /// [Docs](https://tailwindcss.com/docs/flex-direction#row) - fn flex_row(mut self) -> Self - where - Self: Sized, - { + fn flex_row(mut self) -> Self { self.style().flex_direction = Some(FlexDirection::Row); self } /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. /// [Docs](https://tailwindcss.com/docs/flex#flex-1) - fn flex_1(mut self) -> Self - where - Self: Sized, - { + fn flex_1(mut self) -> Self { self.style().flex_grow = Some(1.); self.style().flex_shrink = Some(1.); self.style().flex_basis = Some(relative(0.).into()); @@ -148,10 +126,7 @@ pub trait Styled { /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size. /// [Docs](https://tailwindcss.com/docs/flex#auto) - fn flex_auto(mut self) -> Self - where - Self: Sized, - { + fn flex_auto(mut self) -> Self { self.style().flex_grow = Some(1.); self.style().flex_shrink = Some(1.); self.style().flex_basis = Some(Length::Auto); @@ -160,10 +135,7 @@ pub trait Styled { /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size. /// [Docs](https://tailwindcss.com/docs/flex#initial) - fn flex_initial(mut self) -> Self - where - Self: Sized, - { + fn flex_initial(mut self) -> Self { self.style().flex_grow = Some(0.); self.style().flex_shrink = Some(1.); self.style().flex_basis = Some(Length::Auto); @@ -172,10 +144,7 @@ pub trait Styled { /// Sets the element to prevent a flex item from growing or shrinking. /// [Docs](https://tailwindcss.com/docs/flex#none) - fn flex_none(mut self) -> Self - where - Self: Sized, - { + fn flex_none(mut self) -> Self { self.style().flex_grow = Some(0.); self.style().flex_shrink = Some(0.); self @@ -183,40 +152,28 @@ pub trait Styled { /// Sets the element to allow a flex item to grow to fill any available space. /// [Docs](https://tailwindcss.com/docs/flex-grow) - fn grow(mut self) -> Self - where - Self: Sized, - { + fn grow(mut self) -> Self { self.style().flex_grow = Some(1.); self } /// Sets the element to align flex items to the start of the container's cross axis. /// [Docs](https://tailwindcss.com/docs/align-items#start) - fn items_start(mut self) -> Self - where - Self: Sized, - { + fn items_start(mut self) -> Self { self.style().align_items = Some(AlignItems::FlexStart); self } /// Sets the element to align flex items to the end of the container's cross axis. /// [Docs](https://tailwindcss.com/docs/align-items#end) - fn items_end(mut self) -> Self - where - Self: Sized, - { + fn items_end(mut self) -> Self { self.style().align_items = Some(AlignItems::FlexEnd); self } /// Sets the element to align flex items along the center of the container's cross axis. /// [Docs](https://tailwindcss.com/docs/align-items#center) - fn items_center(mut self) -> Self - where - Self: Sized, - { + fn items_center(mut self) -> Self { self.style().align_items = Some(AlignItems::Center); self } @@ -224,40 +181,28 @@ pub trait Styled { /// Sets the element to justify flex items along the container's main axis /// such that there is an equal amount of space between each item. /// [Docs](https://tailwindcss.com/docs/justify-content#space-between) - fn justify_between(mut self) -> Self - where - Self: Sized, - { + fn justify_between(mut self) -> Self { self.style().justify_content = Some(JustifyContent::SpaceBetween); self } /// Sets the element to justify flex items along the center of the container's main axis. /// [Docs](https://tailwindcss.com/docs/justify-content#center) - fn justify_center(mut self) -> Self - where - Self: Sized, - { + fn justify_center(mut self) -> Self { self.style().justify_content = Some(JustifyContent::Center); self } /// Sets the element to justify flex items against the start of the container's main axis. /// [Docs](https://tailwindcss.com/docs/justify-content#start) - fn justify_start(mut self) -> Self - where - Self: Sized, - { + fn justify_start(mut self) -> Self { self.style().justify_content = Some(JustifyContent::Start); self } /// Sets the element to justify flex items against the end of the container's main axis. /// [Docs](https://tailwindcss.com/docs/justify-content#end) - fn justify_end(mut self) -> Self - where - Self: Sized, - { + fn justify_end(mut self) -> Self { self.style().justify_content = Some(JustifyContent::End); self } @@ -265,10 +210,7 @@ pub trait Styled { /// Sets the element to justify items along the container's main axis such /// that there is an equal amount of space on each side of each item. /// [Docs](https://tailwindcss.com/docs/justify-content#space-around) - fn justify_around(mut self) -> Self - where - Self: Sized, - { + fn justify_around(mut self) -> Self { self.style().justify_content = Some(JustifyContent::SpaceAround); self } @@ -295,30 +237,21 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self - where - Self: Sized, - { + fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self { self.style().box_shadow = Some(shadows); self } /// Clears the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_none(mut self) -> Self - where - Self: Sized, - { + fn shadow_none(mut self) -> Self { self.style().box_shadow = Some(Default::default()); self } /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_sm(mut self) -> Self - where - Self: Sized, - { + fn shadow_sm(mut self) -> Self { self.style().box_shadow = Some(smallvec::smallvec![BoxShadow { color: hsla(0., 0., 0., 0.05), offset: point(px(0.), px(1.)), @@ -330,10 +263,7 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_md(mut self) -> Self - where - Self: Sized, - { + fn shadow_md(mut self) -> Self { self.style().box_shadow = Some(smallvec![ BoxShadow { color: hsla(0.5, 0., 0., 0.1), @@ -353,10 +283,7 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_lg(mut self) -> Self - where - Self: Sized, - { + fn shadow_lg(mut self) -> Self { self.style().box_shadow = Some(smallvec![ BoxShadow { color: hsla(0., 0., 0., 0.1), @@ -376,10 +303,7 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_xl(mut self) -> Self - where - Self: Sized, - { + fn shadow_xl(mut self) -> Self { self.style().box_shadow = Some(smallvec![ BoxShadow { color: hsla(0., 0., 0., 0.1), @@ -399,10 +323,7 @@ pub trait Styled { /// Sets the box shadow of the element. /// [Docs](https://tailwindcss.com/docs/box-shadow) - fn shadow_2xl(mut self) -> Self - where - Self: Sized, - { + fn shadow_2xl(mut self) -> Self { self.style().box_shadow = Some(smallvec![BoxShadow { color: hsla(0., 0., 0., 0.25), offset: point(px(0.), px(25.)), @@ -417,198 +338,138 @@ pub trait Styled { &mut style.text } - fn text_color(mut self, color: impl Into) -> Self - where - Self: Sized, - { + fn text_color(mut self, color: impl Into) -> Self { self.text_style().get_or_insert_with(Default::default).color = Some(color.into()); self } - fn text_size(mut self, size: impl Into) -> Self - where - Self: Sized, - { + fn text_size(mut self, size: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(size.into()); self } - fn text_xs(mut self) -> Self - where - Self: Sized, - { + fn text_xs(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(0.75).into()); self } - fn text_sm(mut self) -> Self - where - Self: Sized, - { + fn text_sm(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(0.875).into()); self } - fn text_base(mut self) -> Self - where - Self: Sized, - { + fn text_base(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.0).into()); self } - fn text_lg(mut self) -> Self - where - Self: Sized, - { + fn text_lg(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.125).into()); self } - fn text_xl(mut self) -> Self - where - Self: Sized, - { + fn text_xl(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.25).into()); self } - fn text_2xl(mut self) -> Self - where - Self: Sized, - { + fn text_2xl(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.5).into()); self } - fn text_3xl(mut self) -> Self - where - Self: Sized, - { + fn text_3xl(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_size = Some(rems(1.875).into()); self } - fn text_decoration_none(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_none(mut self) -> Self { self.text_style() .get_or_insert_with(Default::default) .underline = None; self } - fn text_decoration_color(mut self, color: impl Into) -> Self - where - Self: Sized, - { + fn text_decoration_color(mut self, color: impl Into) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.color = Some(color.into()); self } - fn text_decoration_solid(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_solid(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.wavy = false; self } - fn text_decoration_wavy(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_wavy(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.wavy = true; self } - fn text_decoration_0(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_0(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(0.); self } - fn text_decoration_1(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_1(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(1.); self } - fn text_decoration_2(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_2(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(2.); self } - fn text_decoration_4(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_4(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(4.); self } - fn text_decoration_8(mut self) -> Self - where - Self: Sized, - { + fn text_decoration_8(mut self) -> Self { let style = self.text_style().get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default); underline.thickness = px(8.); self } - fn font(mut self, family_name: impl Into) -> Self - where - Self: Sized, - { + fn font(mut self, family_name: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) .font_family = Some(family_name.into()); self } - fn line_height(mut self, line_height: impl Into) -> Self - where - Self: Sized, - { + fn line_height(mut self, line_height: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) .line_height = Some(line_height.into()); diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index e8d6acc5a3e965818fd9198783c289297588f573..c7031fcb4d77c86ebe765c1021468f9751fcc96e 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -39,6 +39,7 @@ pub struct TextSystem { platform_text_system: Arc, font_ids_by_font: RwLock>, font_metrics: RwLock>, + raster_bounds: RwLock>>, wrapper_pool: Mutex>>, font_runs_pool: Mutex>>, } @@ -48,10 +49,11 @@ impl TextSystem { TextSystem { line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())), platform_text_system, - font_metrics: RwLock::new(HashMap::default()), - font_ids_by_font: RwLock::new(HashMap::default()), - wrapper_pool: Mutex::new(HashMap::default()), - font_runs_pool: Default::default(), + font_metrics: RwLock::default(), + raster_bounds: RwLock::default(), + font_ids_by_font: RwLock::default(), + wrapper_pool: Mutex::default(), + font_runs_pool: Mutex::default(), } } @@ -252,14 +254,24 @@ impl TextSystem { } pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result> { - self.platform_text_system.glyph_raster_bounds(params) + let raster_bounds = self.raster_bounds.upgradable_read(); + if let Some(bounds) = raster_bounds.get(params) { + Ok(bounds.clone()) + } else { + let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds); + let bounds = self.platform_text_system.glyph_raster_bounds(params)?; + raster_bounds.insert(params.clone(), bounds); + Ok(bounds) + } } pub fn rasterize_glyph( &self, - glyph_id: &RenderGlyphParams, + params: &RenderGlyphParams, ) -> Result<(Size, Vec)> { - self.platform_text_system.rasterize_glyph(glyph_id) + let raster_bounds = self.raster_bounds(params)?; + self.platform_text_system + .rasterize_glyph(params, raster_bounds) } } diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 38120a3e37919e8e6ddac33214f2098bdaa06602..c0bbe00db632023d6078bbeaedc06e57c2db7415 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -206,7 +206,7 @@ impl From> for AnyView { impl Element for AnyView { type ElementState = Box; - fn id(&self) -> Option { + fn element_id(&self) -> Option { Some(self.model.entity_id.into()) } @@ -305,7 +305,7 @@ where { type ElementState = AnyElement; - fn id(&self) -> Option { + fn element_id(&self) -> Option { Some(self.view.entity_id().into()) } @@ -315,7 +315,7 @@ where _: Option, cx: &mut ViewContext, ) -> Self::ElementState { - cx.with_element_id(self.view.entity_id(), |_, cx| { + cx.with_element_id(Some(self.view.entity_id()), |cx| { self.view.update(cx, |view, cx| { let mut element = self.component.take().unwrap().render(); element.initialize(view, cx); @@ -330,7 +330,7 @@ where element: &mut Self::ElementState, cx: &mut ViewContext, ) -> LayoutId { - cx.with_element_id(self.view.entity_id(), |_, cx| { + cx.with_element_id(Some(self.view.entity_id()), |cx| { self.view.update(cx, |view, cx| element.layout(view, cx)) }) } @@ -342,7 +342,7 @@ where element: &mut Self::ElementState, cx: &mut ViewContext, ) { - cx.with_element_id(self.view.entity_id(), |_, cx| { + cx.with_element_id(Some(self.view.entity_id()), |cx| { self.view.update(cx, |view, cx| element.paint(view, cx)) }) } @@ -364,7 +364,7 @@ mod any_view { use std::any::Any; pub(crate) fn initialize(view: &AnyView, cx: &mut WindowContext) -> Box { - cx.with_element_id(view.model.entity_id, |_, cx| { + cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); let element = view.update(cx, |view, cx| { let mut element = AnyElement::new(view.render(cx)); @@ -380,7 +380,7 @@ mod any_view { element: &mut Box, cx: &mut WindowContext, ) -> LayoutId { - cx.with_element_id(view.model.entity_id, |_, cx| { + 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.layout(view, cx)) @@ -392,7 +392,7 @@ mod any_view { element: &mut Box, cx: &mut WindowContext, ) { - cx.with_element_id(view.model.entity_id, |_, cx| { + 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)) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index eb69b451b3af924a909004ec1addebb539e48e19..c177ffc8c21df41b343f5f588ddeb2fc99ac1ea4 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1074,7 +1074,7 @@ impl<'a> WindowContext<'a> { if let Some(active_drag) = self.app.active_drag.take() { self.with_z_index(1, |cx| { let offset = cx.mouse_position() - active_drag.cursor_offset; - cx.with_element_offset(Some(offset), |cx| { + cx.with_element_offset(offset, |cx| { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); active_drag.view.draw(available_space, cx); @@ -1083,7 +1083,7 @@ impl<'a> WindowContext<'a> { }); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { self.with_z_index(1, |cx| { - cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| { + cx.with_element_offset(active_tooltip.cursor_offset, |cx| { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); active_tooltip.view.draw(available_space, cx); @@ -1365,6 +1365,14 @@ impl<'a> WindowContext<'a> { self.window.platform_window.activate(); } + pub fn minimize_window(&self) { + self.window.platform_window.minimize(); + } + + pub fn toggle_full_screen(&self) { + self.window.platform_window.toggle_full_screen(); + } + pub fn prompt( &self, level: PromptLevel, @@ -1573,43 +1581,50 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { /// used to associate state with identified elements across separate frames. fn with_element_id( &mut self, - id: impl Into, - f: impl FnOnce(GlobalElementId, &mut Self) -> R, + id: Option>, + f: impl FnOnce(&mut Self) -> R, ) -> R { - let window = self.window_mut(); - window.element_id_stack.push(id.into()); - let global_id = window.element_id_stack.clone(); - let result = f(global_id, self); - let window: &mut Window = self.borrow_mut(); - window.element_id_stack.pop(); - result + if let Some(id) = id.map(Into::into) { + let window = self.window_mut(); + window.element_id_stack.push(id.into()); + let result = f(self); + let window: &mut Window = self.borrow_mut(); + window.element_id_stack.pop(); + result + } else { + f(self) + } } /// Invoke the given function with the given content mask after intersecting it /// with the current mask. fn with_content_mask( &mut self, - mask: ContentMask, + mask: Option>, f: impl FnOnce(&mut Self) -> R, ) -> R { - let mask = mask.intersect(&self.content_mask()); - self.window_mut() - .current_frame - .content_mask_stack - .push(mask); - let result = f(self); - self.window_mut().current_frame.content_mask_stack.pop(); - result + if let Some(mask) = mask { + let mask = mask.intersect(&self.content_mask()); + self.window_mut() + .current_frame + .content_mask_stack + .push(mask); + let result = f(self); + self.window_mut().current_frame.content_mask_stack.pop(); + result + } else { + f(self) + } } /// Update the global element offset based on the given offset. This is used to implement /// scrolling and position drag handles. fn with_element_offset( &mut self, - offset: Option>, + offset: Point, f: impl FnOnce(&mut Self) -> R, ) -> R { - let Some(offset) = offset else { + if offset.is_zero() { return f(self); }; @@ -1645,7 +1660,9 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { where S: 'static, { - self.with_element_id(id, |global_id, cx| { + self.with_element_id(Some(id), |cx| { + let global_id = cx.window().element_id_stack.clone(); + if let Some(any) = cx .window_mut() .current_frame @@ -2073,11 +2090,10 @@ impl<'a, V: 'static> ViewContext<'a, V> { f: impl FnOnce(Option, &mut Self) -> R, ) -> R { let window = &mut self.window; - window .current_frame .dispatch_tree - .push_node(context, &mut window.previous_frame.dispatch_tree); + .push_node(context.clone(), &mut window.previous_frame.dispatch_tree); if let Some(focus_handle) = focus_handle.as_ref() { window .current_frame @@ -2360,6 +2376,12 @@ impl WindowHandle { { cx.read_window(self, |root_view, _cx| root_view.clone()) } + + pub fn is_active(&self, cx: &WindowContext) -> Option { + cx.windows + .get(self.id) + .and_then(|window| window.as_ref().map(|window| window.active)) + } } impl Copy for WindowHandle {} diff --git a/crates/gpui2_macros/src/style_helpers.rs b/crates/gpui2_macros/src/style_helpers.rs index 57aef03afa55276293d48f610458c5d29ddf9862..181311807c8832a3e48dc05c445e489b7c9b22b0 100644 --- a/crates/gpui2_macros/src/style_helpers.rs +++ b/crates/gpui2_macros/src/style_helpers.rs @@ -130,7 +130,7 @@ fn generate_predefined_setter( let method = quote! { #[doc = #doc_string] - fn #method_name(mut self) -> Self where Self: std::marker::Sized { + fn #method_name(mut self) -> Self { let style = self.style(); #(#field_assignments)* self @@ -163,7 +163,7 @@ fn generate_custom_value_setter( let method = quote! { #[doc = #doc_string] - fn #method_name(mut self, length: impl std::clone::Clone + Into) -> Self where Self: std::marker::Sized { + fn #method_name(mut self, length: impl std::clone::Clone + Into) -> Self { let style = self.style(); #(#field_assignments)* self diff --git a/crates/install_cli2/Cargo.toml b/crates/install_cli2/Cargo.toml index 3310e7fbc8085d0e14c05f4b37a2a6540959585f..26fe212fe3906ee8d0407d55d81ae7f195fe6c9f 100644 --- a/crates/install_cli2/Cargo.toml +++ b/crates/install_cli2/Cargo.toml @@ -14,5 +14,6 @@ test-support = [] smol.workspace = true anyhow.workspace = true log.workspace = true +serde.workspace = true gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } diff --git a/crates/install_cli2/src/install_cli2.rs b/crates/install_cli2/src/install_cli2.rs index 7938d602105488729d62aafae7e4092adf3995d6..6fd1019c3f938e0c6c9cd96476075a65572a5dc7 100644 --- a/crates/install_cli2/src/install_cli2.rs +++ b/crates/install_cli2/src/install_cli2.rs @@ -1,10 +1,9 @@ use anyhow::{anyhow, Result}; -use gpui::AsyncAppContext; +use gpui::{actions, AsyncAppContext}; use std::path::Path; use util::ResultExt; -// todo!() -// actions!(cli, [Install]); +actions!(Install); pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??; diff --git a/crates/live_kit_client2/examples/test_app.rs b/crates/live_kit_client2/examples/test_app.rs index 98302eb35c049015cbfee131b5dbcf125b7f5956..0b9e54f9b0dc7801f695adcf31b352d9997a48f1 100644 --- a/crates/live_kit_client2/examples/test_app.rs +++ b/crates/live_kit_client2/examples/test_app.rs @@ -167,7 +167,7 @@ fn main() { panic!("unexpected message"); } - cx.update(|cx| cx.quit()).ok(); + cx.update(|cx| cx.shutdown()).ok(); }) .detach(); }); diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index a099a025e657922fa96bc8ba66de60beacd6911b..2621c58120af8ae3e711637af7a941dcc24af150 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use serde::Deserialize; -use smol::{fs, io::BufReader, process::Command}; +use smol::{fs, io::BufReader, lock::Mutex, process::Command}; use std::process::{Output, Stdio}; use std::{ env::consts, @@ -45,14 +45,19 @@ pub trait NodeRuntime: Send + Sync { pub struct RealNodeRuntime { http: Arc, + installation_lock: Mutex<()>, } impl RealNodeRuntime { pub fn new(http: Arc) -> Arc { - Arc::new(RealNodeRuntime { http }) + Arc::new(RealNodeRuntime { + http, + installation_lock: Mutex::new(()), + }) } async fn install_if_needed(&self) -> Result { + let _lock = self.installation_lock.lock().await; log::info!("Node runtime install_if_needed"); let arch = match consts::ARCH { @@ -73,6 +78,9 @@ impl RealNodeRuntime { .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) + .args(["--cache".into(), node_dir.join("cache")]) + .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")]) + .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]) .status() .await; let valid = matches!(result, Ok(status) if status.success()); @@ -96,6 +104,11 @@ impl RealNodeRuntime { archive.unpack(&node_containing_dir).await?; } + // Note: Not in the `if !valid {}` so we can populate these for existing installations + _ = fs::create_dir(node_dir.join("cache")).await; + _ = fs::write(node_dir.join("blank_user_npmrc"), []).await; + _ = fs::write(node_dir.join("blank_global_npmrc"), []).await; + anyhow::Ok(node_dir) } } @@ -137,7 +150,17 @@ impl NodeRuntime for RealNodeRuntime { let mut command = Command::new(node_binary); command.env("PATH", env_path); - command.arg(npm_file).arg(subcommand).args(args); + command.arg(npm_file).arg(subcommand); + command.args(["--cache".into(), installation_path.join("cache")]); + command.args([ + "--userconfig".into(), + installation_path.join("blank_user_npmrc"), + ]); + command.args([ + "--globalconfig".into(), + installation_path.join("blank_global_npmrc"), + ]); + command.args(args); if let Some(directory) = directory { command.current_dir(directory); diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 0cfe5c8992a49a6c56650f02c73845dfd5e1c337..72a2f812e974d7f45d5b1110d4c26ac093e78f29 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,10 +1,10 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, MouseButton, ParentElement, Render, StatelessInteractive, - Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, + div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task, + UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; -use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; +use ui::{prelude::*, v_stack, Divider, Label, TextColor}; pub struct Picker { pub delegate: D, @@ -58,7 +58,7 @@ impl Picker { self.editor.update(cx, |editor, cx| editor.focus(cx)); } - fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { + pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); @@ -98,6 +98,15 @@ impl Picker { } } + pub fn cycle_selection(&mut self, cx: &mut ViewContext) { + let count = self.delegate.match_count(); + let index = self.delegate.selected_index(); + let new_index = if index + 1 == count { 0 } else { index + 1 }; + self.delegate.set_selected_index(new_index, cx); + self.scroll_handle.scroll_to_item(new_index); + cx.notify(); + } + fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { self.delegate.dismissed(cx); } @@ -137,6 +146,11 @@ impl Picker { } } + pub fn refresh(&mut self, cx: &mut ViewContext) { + let query = self.editor.read(cx).text(cx); + self.update_matches(query, cx); + } + pub fn update_matches(&mut self, query: String, cx: &mut ViewContext) { let update = self.delegate.update_matches(query, cx); self.matches_updated(cx); @@ -165,7 +179,7 @@ impl Render for Picker { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() - .context("picker") + .key_context("picker") .size_full() .elevation_2(cx) .on_action(Self::select_next) @@ -224,7 +238,7 @@ impl Render for Picker { v_stack().p_1().grow().child( div() .px_1() - .child(Label::new("No matches").color(LabelColor::Muted)), + .child(Label::new("No matches").color(TextColor::Muted)), ), ) }) diff --git a/crates/project_panel2/Cargo.toml b/crates/project_panel2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..bd6bc59a652c9945280233bd8588a15ea25834e4 --- /dev/null +++ b/crates/project_panel2/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "project_panel2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/project_panel.rs" +doctest = false + +[dependencies] +context_menu = { path = "../context_menu" } +collections = { path = "../collections" } +db = { path = "../db2", package = "db2" } +editor = { path = "../editor2", package = "editor2" } +gpui = { path = "../gpui2", package = "gpui2" } +menu = { path = "../menu2", package = "menu2" } +project = { path = "../project2", package = "project2" } +settings = { path = "../settings2", package = "settings2" } +theme = { path = "../theme2", package = "theme2" } +ui = { path = "../ui2", package = "ui2" } +util = { path = "../util" } +workspace = { path = "../workspace2", package = "workspace2" } +anyhow.workspace = true +postage.workspace = true +futures.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +schemars.workspace = true +smallvec.workspace = true +pretty_assertions.workspace = true +unicase = "2.6" + +[dev-dependencies] +client = { path = "../client2", package = "client2", features = ["test-support"] } +language = { path = "../language2", package = "language2", features = ["test-support"] } +editor = { path = "../editor2", package = "editor2", features = ["test-support"] } +gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] } +workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] } +serde_json.workspace = true diff --git a/crates/project_panel2/src/file_associations.rs b/crates/project_panel2/src/file_associations.rs new file mode 100644 index 0000000000000000000000000000000000000000..9e9a865f3e78d2c563476ef4982286c1cfc60405 --- /dev/null +++ b/crates/project_panel2/src/file_associations.rs @@ -0,0 +1,96 @@ +use std::{path::Path, str, sync::Arc}; + +use collections::HashMap; + +use gpui::{AppContext, AssetSource}; +use serde_derive::Deserialize; +use util::{maybe, paths::PathExt}; + +#[derive(Deserialize, Debug)] +struct TypeConfig { + icon: Arc, +} + +#[derive(Deserialize, Debug)] +pub struct FileAssociations { + suffixes: HashMap, + types: HashMap, +} + +const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder"; +const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder"; +const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron"; +const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron"; +pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json"; + +pub fn init(assets: impl AssetSource, cx: &mut AppContext) { + cx.set_global(FileAssociations::new(assets)) +} + +impl FileAssociations { + pub fn new(assets: impl AssetSource) -> Self { + assets + .load("icons/file_icons/file_types.json") + .and_then(|file| { + serde_json::from_str::(str::from_utf8(&file).unwrap()) + .map_err(Into::into) + }) + .unwrap_or_else(|_| FileAssociations { + suffixes: HashMap::default(), + types: HashMap::default(), + }) + } + + pub fn get_icon(path: &Path, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + // FIXME: Associate a type with the languages and have the file's langauge + // override these associations + maybe!({ + let suffix = path.icon_suffix()?; + + this.suffixes + .get(suffix) + .and_then(|type_str| this.types.get(type_str)) + .map(|type_config| type_config.icon.clone()) + }) + .or_else(|| this.types.get("default").map(|config| config.icon.clone())) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } + + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_DIRECTORY_TYPE + } else { + COLLAPSED_DIRECTORY_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } + + pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_CHEVRON_TYPE + } else { + COLLAPSED_CHEVRON_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } +} diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index e3e04f5254f84009b86d02f59f9ce9894a3ab8b7..79a0f344aebb0191a9bebc3b640c18ce2d33fb17 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -8,11 +8,11 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ - actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, - AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusEnabled, - FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, - StatefulInteractive, StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, + ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent, + Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful, + StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext, + VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -21,7 +21,6 @@ use project::{ }; use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; use std::{ cmp::Ordering, collections::{hash_map, HashMap}, @@ -31,9 +30,9 @@ use std::{ sync::Arc, }; use theme::ActiveTheme as _; -use ui::{h_stack, v_stack}; +use ui::{h_stack, v_stack, IconElement, Label}; use unicase::UniCase; -use util::TryFutureExt; +use util::{maybe, TryFutureExt}; use workspace::{ dock::{DockPosition, PanelEvent}, Workspace, @@ -54,8 +53,8 @@ pub struct ProjectPanel { edit_state: Option, filename_editor: View, clipboard_entry: Option, - dragged_entry_destination: Option>, - workspace: WeakView, + _dragged_entry_destination: Option>, + _workspace: WeakView, has_focus: bool, width: Option, pending_serialization: Task>, @@ -131,31 +130,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_associations::init(assets, cx); - - // cx.add_action(ProjectPanel::expand_selected_entry); - // cx.add_action(ProjectPanel::collapse_selected_entry); - // cx.add_action(ProjectPanel::collapse_all_entries); - // cx.add_action(ProjectPanel::select_prev); - // cx.add_action(ProjectPanel::select_next); - // cx.add_action(ProjectPanel::new_file); - // cx.add_action(ProjectPanel::new_directory); - // cx.add_action(ProjectPanel::rename); - // cx.add_async_action(ProjectPanel::delete); - // cx.add_async_action(ProjectPanel::confirm); - // cx.add_async_action(ProjectPanel::open_file); - // cx.add_action(ProjectPanel::cancel); - // cx.add_action(ProjectPanel::cut); - // cx.add_action(ProjectPanel::copy); - // cx.add_action(ProjectPanel::copy_path); - // cx.add_action(ProjectPanel::copy_relative_path); - // cx.add_action(ProjectPanel::reveal_in_finder); - // cx.add_action(ProjectPanel::open_in_terminal); - // cx.add_action(ProjectPanel::new_search_in_directory); - // cx.add_action( - // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { - // this.paste(action, cx); - // }, - // ); } #[derive(Debug)] @@ -222,29 +196,25 @@ impl ProjectPanel { editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { this.autoscroll(cx); } + editor::Event::Blurred => { + if this + .edit_state + .as_ref() + .map_or(false, |state| state.processing_filename.is_none()) + { + this.edit_state = None; + this.update_visible_entries(None, cx); + } + } _ => {} }) .detach(); - // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| { - // if !is_focused - // && this - // .edit_state - // .as_ref() - // .map_or(false, |state| state.processing_filename.is_none()) - // { - // this.edit_state = None; - // this.update_visible_entries(None, cx); - // } - // }) - // .detach(); - // cx.observe_global::(|_, cx| { // cx.notify(); // }) // .detach(); - let view_id = cx.view().entity_id(); let mut this = Self { project: project.clone(), fs: workspace.app_state().fs.clone(), @@ -258,8 +228,8 @@ impl ProjectPanel { filename_editor, clipboard_entry: None, // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), - dragged_entry_destination: None, - workspace: workspace.weak_handle(), + _dragged_entry_destination: None, + _workspace: workspace.weak_handle(), has_focus: false, width: None, pending_serialization: Task::ready(None), @@ -311,19 +281,19 @@ impl ProjectPanel { } } &Event::SplitEntry { entry_id } => { - // if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - // if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { - // workspace - // .split_path( - // ProjectPath { - // worktree_id: worktree.read(cx).id(), - // path: entry.path.clone(), - // }, - // cx, - // ) - // .detach_and_log_err(cx); - // } - // } + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { + if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) { + // workspace + // .split_path( + // ProjectPath { + // worktree_id: worktree.read(cx).id(), + // path: entry.path.clone(), + // }, + // cx, + // ) + // .detach_and_log_err(cx); + } + } } _ => {} } @@ -391,79 +361,80 @@ impl ProjectPanel { fn deploy_context_menu( &mut self, - position: Point, - entry_id: ProjectEntryId, - cx: &mut ViewContext, + _position: Point, + _entry_id: ProjectEntryId, + _cx: &mut ViewContext, ) { - // let project = self.project.read(cx); - - // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { - // id - // } else { - // return; - // }; + todo!() + // let project = self.project.read(cx); - // self.selection = Some(Selection { - // worktree_id, - // entry_id, - // }); + // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { + // id + // } else { + // return; + // }; - // let mut menu_entries = Vec::new(); - // if let Some((worktree, entry)) = self.selected_entry(cx) { - // let is_root = Some(entry) == worktree.root_entry(); - // if !project.is_remote() { - // menu_entries.push(ContextMenuItem::action( - // "Add Folder to Project", - // workspace::AddFolderToProject, - // )); - // if is_root { - // let project = self.project.clone(); - // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { - // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - // })); + // self.selection = Some(Selection { + // worktree_id, + // entry_id, + // }); + + // let mut menu_entries = Vec::new(); + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let is_root = Some(entry) == worktree.root_entry(); + // if !project.is_remote() { + // menu_entries.push(ContextMenuItem::action( + // "Add Folder to Project", + // workspace::AddFolderToProject, + // )); + // if is_root { + // let project = self.project.clone(); + // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { + // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + // })); + // } // } - // } - // menu_entries.push(ContextMenuItem::action("New File", NewFile)); - // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Cut", Cut)); - // menu_entries.push(ContextMenuItem::action("Copy", Copy)); - // if let Some(clipboard_entry) = self.clipboard_entry { - // if clipboard_entry.worktree_id() == worktree.id() { - // menu_entries.push(ContextMenuItem::action("Paste", Paste)); + // menu_entries.push(ContextMenuItem::action("New File", NewFile)); + // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Cut", Cut)); + // menu_entries.push(ContextMenuItem::action("Copy", Copy)); + // if let Some(clipboard_entry) = self.clipboard_entry { + // if clipboard_entry.worktree_id() == worktree.id() { + // menu_entries.push(ContextMenuItem::action("Paste", Paste)); + // } // } - // } - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); - // menu_entries.push(ContextMenuItem::action( - // "Copy Relative Path", - // CopyRelativePath, - // )); - - // if entry.is_dir() { // menu_entries.push(ContextMenuItem::Separator); - // } - // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); - // if entry.is_dir() { - // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); + // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); // menu_entries.push(ContextMenuItem::action( - // "Search Inside", - // NewSearchInDirectory, + // "Copy Relative Path", + // CopyRelativePath, // )); - // } - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Rename", Rename)); - // if !is_root { - // menu_entries.push(ContextMenuItem::action("Delete", Delete)); + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::Separator); + // } + // menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); + // if entry.is_dir() { + // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); + // menu_entries.push(ContextMenuItem::action( + // "Search Inside", + // NewSearchInDirectory, + // )); + // } + + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Rename", Rename)); + // if !is_root { + // menu_entries.push(ContextMenuItem::action("Delete", Delete)); + // } // } - // } - // // self.context_menu.update(cx, |menu, cx| { - // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); - // // }); + // // self.context_menu.update(cx, |menu, cx| { + // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); + // // }); - // cx.notify(); + // cx.notify(); } fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { @@ -579,22 +550,18 @@ impl ProjectPanel { } } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(task) = self.confirm_edit(cx) { - return Some(task); + task.detach_and_log_err(cx); } - - None } - fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) { if let Some((_, entry)) = self.selected_entry(cx) { if entry.is_file() { self.open_entry(entry.id, true, cx); } } - - None } fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { @@ -800,27 +767,32 @@ impl ProjectPanel { } } - fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { - let Selection { entry_id, .. } = self.selection?; - let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; - let file_name = path.file_name()?; - - let mut answer = cx.prompt( - PromptLevel::Info, - &format!("Delete {file_name:?}?"), - &["Delete", "Cancel"], - ); - Some(cx.spawn(|this, mut cx| async move { - if answer.await != Ok(0) { - return Ok(()); - } - this.update(&mut cx, |this, cx| { - this.project - .update(cx, |project, cx| project.delete_entry(entry_id, cx)) - .ok_or_else(|| anyhow!("no such entry")) - })?? - .await - })) + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { + maybe!({ + let Selection { entry_id, .. } = self.selection?; + let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; + let file_name = path.file_name()?; + + let answer = cx.prompt( + PromptLevel::Info, + &format!("Delete {file_name:?}?"), + &["Delete", "Cancel"], + ); + + cx.spawn(|this, mut cx| async move { + if answer.await != Ok(0) { + return Ok(()); + } + this.update(&mut cx, |this, cx| { + this.project + .update(cx, |project, cx| project.delete_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("no such entry")) + })?? + .await + }) + .detach_and_log_err(cx); + Some(()) + }); } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { @@ -897,8 +869,9 @@ impl ProjectPanel { } } - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { - if let Some((worktree, entry)) = self.selected_entry(cx) { + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + maybe!({ + let (worktree, entry) = self.selected_entry(cx)?; let clipboard_entry = self.clipboard_entry?; if clipboard_entry.worktree_id() != worktree.id() { return None; @@ -942,15 +915,16 @@ impl ProjectPanel { if let Some(task) = self.project.update(cx, |project, cx| { project.rename_entry(clipboard_entry.entry_id(), new_path, cx) }) { - task.detach_and_log_err(cx) + task.detach_and_log_err(cx); } } else if let Some(task) = self.project.update(cx, |project, cx| { project.copy_entry(clipboard_entry.entry_id(), new_path, cx) }) { - task.detach_and_log_err(cx) + task.detach_and_log_err(cx); } - } - None + + Some(()) + }); } fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { @@ -977,7 +951,7 @@ impl ProjectPanel { } } - fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext) { todo!() // if let Some((worktree, entry)) = self.selected_entry(cx) { // let window = cx.window(); @@ -1012,36 +986,37 @@ impl ProjectPanel { } } - fn move_entry( - &mut self, - entry_to_move: ProjectEntryId, - destination: ProjectEntryId, - destination_is_file: bool, - cx: &mut ViewContext, - ) { - let destination_worktree = self.project.update(cx, |project, cx| { - let entry_path = project.path_for_entry(entry_to_move, cx)?; - let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); - - let mut destination_path = destination_entry_path.as_ref(); - if destination_is_file { - destination_path = destination_path.parent()?; - } - - let mut new_path = destination_path.to_path_buf(); - new_path.push(entry_path.path.file_name()?); - if new_path != entry_path.path.as_ref() { - let task = project.rename_entry(entry_to_move, new_path, cx)?; - cx.foreground_executor().spawn(task).detach_and_log_err(cx); - } - - Some(project.worktree_id_for_entry(destination, cx)?) - }); - - if let Some(destination_worktree) = destination_worktree { - self.expand_entry(destination_worktree, destination, cx); - } - } + // todo!() + // fn move_entry( + // &mut self, + // entry_to_move: ProjectEntryId, + // destination: ProjectEntryId, + // destination_is_file: bool, + // cx: &mut ViewContext, + // ) { + // let destination_worktree = self.project.update(cx, |project, cx| { + // let entry_path = project.path_for_entry(entry_to_move, cx)?; + // let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + + // let mut destination_path = destination_entry_path.as_ref(); + // if destination_is_file { + // destination_path = destination_path.parent()?; + // } + + // let mut new_path = destination_path.to_path_buf(); + // new_path.push(entry_path.path.file_name()?); + // if new_path != entry_path.path.as_ref() { + // let task = project.rename_entry(entry_to_move, new_path, cx)?; + // cx.foreground_executor().spawn(task).detach_and_log_err(cx); + // } + + // Some(project.worktree_id_for_entry(destination, cx)?) + // }); + + // if let Some(destination_worktree) = destination_worktree { + // self.expand_entry(destination_worktree, destination, cx); + // } + // } fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { let mut entry_index = 0; @@ -1366,23 +1341,25 @@ impl ProjectPanel { .git_status .as_ref() .map(|status| match status { - GitFileStatus::Added => theme.styles.status.created, - GitFileStatus::Modified => theme.styles.status.modified, - GitFileStatus::Conflict => theme.styles.status.conflict, + GitFileStatus::Added => theme.status().created, + GitFileStatus::Modified => theme.status().modified, + GitFileStatus::Conflict => theme.status().conflict, }) - .unwrap_or(theme.styles.status.info); + .unwrap_or(theme.status().info); h_stack() .child(if let Some(icon) = &details.icon { - div().child(svg().path(icon.to_string())) + div().child(IconElement::from_path(icon.to_string())) } else { div() }) .child( if let (Some(editor), true) = (editor, show_editor) { - div().child(editor.clone()) + div().w_full().child(editor.clone()) } else { - div().child(details.filename.clone()) + div() + .text_color(filename_text_color) + .child(Label::new(details.filename.clone())) } .ml_1(), ) @@ -1390,21 +1367,29 @@ impl ProjectPanel { } fn render_entry( + &self, entry_id: ProjectEntryId, details: EntryDetails, - editor: &View, // dragged_entry_destination: &mut Option>, - // theme: &theme::ProjectPanel, cx: &mut ViewContext, - ) -> Div> { + ) -> Stateful> { let kind = details.kind; let settings = ProjectPanelSettings::get_global(cx); const INDENT_SIZE: Pixels = px(16.0); let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); let show_editor = details.is_editing && !details.is_processing; + let is_selected = self + .selection + .map_or(false, |selection| selection.entry_id == entry_id); - Self::render_entry_visual_element(&details, Some(editor), padding, cx) + Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx) .id(entry_id.to_proto() as usize) + .w_full() + .cursor_pointer() + .when(is_selected, |this| { + this.bg(cx.theme().colors().element_selected) + }) + .hover(|style| style.bg(cx.theme().colors().element_hover)) .on_click(move |this, event, cx| { if !show_editor { if kind.is_dir() { @@ -1418,38 +1403,51 @@ impl ProjectPanel { } } }) - // .on_down(MouseButton::Right, move |event, this, cx| { - // this.deploy_context_menu(event.position, entry_id, cx); - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_entry)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { + .on_mouse_down(MouseButton::Right, move |this, event, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + }) + // .on_drop::(|this, event, cx| { // this.move_entry( // *dragged_entry, // entry_id, // matches!(details.kind, EntryKind::File(_)), // cx, // ); - // } // }) } } impl Render for ProjectPanel { - type Element = Div, FocusEnabled>; - - fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - enum ProjectPanel {} - let theme = cx.theme(); - let last_worktree_root_id = self.last_worktree_root_id; + type Element = Focusable>>; + fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { let has_worktree = self.visible_entries.len() != 0; if has_worktree { div() .id("project-panel") + .size_full() + .key_context("ProjectPanel") + .on_action(Self::select_next) + .on_action(Self::select_prev) + .on_action(Self::expand_selected_entry) + .on_action(Self::collapse_selected_entry) + .on_action(Self::collapse_all_entries) + .on_action(Self::new_file) + .on_action(Self::new_directory) + .on_action(Self::rename) + .on_action(Self::delete) + .on_action(Self::confirm) + .on_action(Self::open_file) + .on_action(Self::cancel) + .on_action(Self::cut) + .on_action(Self::copy) + .on_action(Self::copy_path) + .on_action(Self::copy_relative_path) + .on_action(Self::paste) + .on_action(Self::reveal_in_finder) + .on_action(Self::open_in_terminal) + .on_action(Self::new_search_in_directory) .track_focus(&self.focus_handle) .child( uniform_list( @@ -1459,19 +1457,14 @@ impl Render for ProjectPanel { .map(|(_, worktree_entries)| worktree_entries.len()) .sum(), |this: &mut Self, range, cx| { - let mut items = SmallVec::new(); + let mut items = Vec::new(); this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - // &mut dragged_entry_destination, - cx, - )); + items.push(this.render_entry(id, details, cx)); }); items }, ) + .size_full() .track_scroll(self.list.clone()), ) } else { @@ -1573,1296 +1566,1315 @@ impl ClipboardEntry { } } -// todo!() -// #[cfg(test)] -// mod tests { -// use super::*; -// use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle}; -// use pretty_assertions::assert_eq; -// use project::FakeFs; -// use serde_json::json; -// use settings::SettingsStore; -// use std::{ -// collections::HashSet, -// path::{Path, PathBuf}, -// sync::atomic::{self, AtomicUsize}, -// }; -// use workspace::{pane, AppState}; - -// #[gpui::test] -// async fn test_visible_list(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.executor().clone()); -// fs.insert_tree( -// "/root1", -// json!({ -// ".dockerignore": "", -// ".git": { -// "HEAD": "", -// }, -// "a": { -// "0": { "q": "", "r": "", "s": "" }, -// "1": { "t": "", "u": "" }, -// "2": { "v": "", "w": "", "x": "", "y": "" }, -// }, -// "b": { -// "3": { "Q": "" }, -// "4": { "R": "", "S": "", "T": "", "U": "" }, -// }, -// "C": { -// "5": {}, -// "6": { "V": "", "W": "" }, -// "7": { "X": "" }, -// "8": { "Y": {}, "Z": "" } -// } -// }), -// ) -// .await; -// fs.insert_tree( -// "/root2", -// json!({ -// "d": { -// "9": "" -// }, -// "e": {} -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// toggle_expand_dir(&panel, "root1/b", cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b <== selected", -// " > 3", -// " > 4", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// assert_eq!( -// visible_entries_as_strings(&panel, 6..9, cx), -// &[ -// // -// " > C", -// " .dockerignore", -// "v root2", -// ] -// ); -// } - -// #[gpui::test(iterations = 30)] -// async fn test_editing_files(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/root1", -// json!({ -// ".dockerignore": "", -// ".git": { -// "HEAD": "", -// }, -// "a": { -// "0": { "q": "", "r": "", "s": "" }, -// "1": { "t": "", "u": "" }, -// "2": { "v": "", "w": "", "x": "", "y": "" }, -// }, -// "b": { -// "3": { "Q": "" }, -// "4": { "R": "", "S": "", "T": "", "U": "" }, -// }, -// "C": { -// "5": {}, -// "6": { "V": "", "W": "" }, -// "7": { "X": "" }, -// "8": { "Y": {}, "Z": "" } -// } -// }), -// ) -// .await; -// fs.insert_tree( -// "/root2", -// json!({ -// "d": { -// "9": "" -// }, -// "e": {} -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// select_path(&panel, "root1", cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1 <== selected", -// " > .git", -// " > a", -// " > b", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// // Add a file with the root folder selected. The filename editor is placed -// // before the first file in the root folder. -// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " [EDITOR: ''] <== selected", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// let confirm = panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); -// panel.confirm(&Confirm, cx).unwrap() -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " [PROCESSING: 'the-new-filename'] <== selected", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// confirm.await.unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " .dockerignore", -// " the-new-filename <== selected", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// select_path(&panel, "root1/b", cx); -// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " [EDITOR: ''] <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// panel -// .update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); -// panel.confirm(&Confirm, cx).unwrap() -// }) -// .await -// .unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " another-filename.txt <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// select_path(&panel, "root1/b/another-filename.txt", cx); -// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " [EDITOR: 'another-filename.txt'] <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// let confirm = panel.update(cx, |panel, cx| { -// panel.filename_editor.update(cx, |editor, cx| { -// let file_name_selections = editor.selections.all::(cx); -// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); -// let file_name_selection = &file_name_selections[0]; -// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); -// assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); - -// editor.set_text("a-different-filename.tar.gz", cx) -// }); -// panel.confirm(&Confirm, cx).unwrap() -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// confirm.await.unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " a-different-filename.tar.gz <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3", -// " > 4", -// " [EDITOR: 'a-different-filename.tar.gz'] <== selected", -// " > C", -// " .dockerignore", -// " the-new-filename", -// ] -// ); - -// panel.update(cx, |panel, cx| { -// panel.filename_editor.update(cx, |editor, cx| { -// let file_name_selections = editor.selections.all::(cx); -// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); -// let file_name_selection = &file_name_selections[0]; -// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); -// assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot"); - -// }); -// panel.cancel(&Cancel, cx) -// }); - -// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > [EDITOR: ''] <== selected", -// " > 3", -// " > 4", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); - -// let confirm = panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("new-dir", cx)); -// panel.confirm(&Confirm, cx).unwrap() -// }); -// panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > [PROCESSING: 'new-dir']", -// " > 3 <== selected", -// " > 4", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); - -// confirm.await.unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3 <== selected", -// " > 4", -// " > new-dir", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); - -// panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx)); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > [EDITOR: '3'] <== selected", -// " > 4", -// " > new-dir", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); - -// // Dismiss the rename editor when it loses focus. -// workspace.update(cx, |_, cx| cx.focus_self()); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " v b", -// " > 3 <== selected", -// " > 4", -// " > new-dir", -// " a-different-filename.tar.gz", -// " > C", -// " .dockerignore", -// ] -// ); -// } - -// #[gpui::test(iterations = 30)] -// async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/root1", -// json!({ -// ".dockerignore": "", -// ".git": { -// "HEAD": "", -// }, -// "a": { -// "0": { "q": "", "r": "", "s": "" }, -// "1": { "t": "", "u": "" }, -// "2": { "v": "", "w": "", "x": "", "y": "" }, -// }, -// "b": { -// "3": { "Q": "" }, -// "4": { "R": "", "S": "", "T": "", "U": "" }, -// }, -// "C": { -// "5": {}, -// "6": { "V": "", "W": "" }, -// "7": { "X": "" }, -// "8": { "Y": {}, "Z": "" } -// } -// }), -// ) -// .await; -// fs.insert_tree( -// "/root2", -// json!({ -// "d": { -// "9": "" -// }, -// "e": {} -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// select_path(&panel, "root1", cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1 <== selected", -// " > .git", -// " > a", -// " > b", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// // Add a file with the root folder selected. The filename editor is placed -// // before the first file in the root folder. -// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " [EDITOR: ''] <== selected", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// let confirm = panel.update(cx, |panel, cx| { -// panel.filename_editor.update(cx, |editor, cx| { -// editor.set_text("/bdir1/dir2/the-new-filename", cx) -// }); -// panel.confirm(&Confirm, cx).unwrap() -// }); - -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " > C", -// " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); - -// confirm.await.unwrap(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..13, cx), -// &[ -// "v root1", -// " > .git", -// " > a", -// " > b", -// " v bdir1", -// " v dir2", -// " the-new-filename <== selected", -// " > C", -// " .dockerignore", -// "v root2", -// " > d", -// " > e", -// ] -// ); -// } - -// #[gpui::test] -// async fn test_copy_paste(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/root1", -// json!({ -// "one.two.txt": "", -// "one.txt": "" -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// panel.update(cx, |panel, cx| { -// panel.select_next(&Default::default(), cx); -// panel.select_next(&Default::default(), cx); -// }); - -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// // -// "v root1", -// " one.two.txt <== selected", -// " one.txt", -// ] -// ); - -// // Regression test - file name is created correctly when -// // the copied file's name contains multiple dots. -// panel.update(cx, |panel, cx| { -// panel.copy(&Default::default(), cx); -// panel.paste(&Default::default(), cx); -// }); -// cx.foreground().run_until_parked(); - -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// // -// "v root1", -// " one.two copy.txt", -// " one.two.txt <== selected", -// " one.txt", -// ] -// ); - -// panel.update(cx, |panel, cx| { -// panel.paste(&Default::default(), cx); -// }); -// cx.foreground().run_until_parked(); - -// assert_eq!( -// visible_entries_as_strings(&panel, 0..50, cx), -// &[ -// // -// "v root1", -// " one.two copy 1.txt", -// " one.two copy.txt", -// " one.two.txt <== selected", -// " one.txt", -// ] -// ); -// } - -// #[gpui::test] -// async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { -// init_test_with_editor(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// toggle_expand_dir(&panel, "src/test", cx); -// select_path(&panel, "src/test/first.rs", cx); -// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs <== selected", -// " second.rs", -// " third.rs" -// ] -// ); -// ensure_single_file_is_opened(window, "test/first.rs", cx); - -// submit_deletion(window.into(), &panel, cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " second.rs", -// " third.rs" -// ], -// "Project panel should have no deleted file, no other file is selected in it" -// ); -// ensure_no_open_items_and_panes(window.into(), &workspace, cx); - -// select_path(&panel, "src/test/second.rs", cx); -// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " second.rs <== selected", -// " third.rs" -// ] -// ); -// ensure_single_file_is_opened(window, "test/second.rs", cx); - -// window.update(cx, |cx| { -// let active_items = workspace -// .read(cx) -// .panes() -// .iter() -// .filter_map(|pane| pane.read(cx).active_item()) -// .collect::>(); -// assert_eq!(active_items.len(), 1); -// let open_editor = active_items -// .into_iter() -// .next() -// .unwrap() -// .downcast::() -// .expect("Open item should be an editor"); -// open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); -// }); -// submit_deletion(window.into(), &panel, cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src", " v test", " third.rs"], -// "Project panel should have no deleted file, with one last file remaining" -// ); -// ensure_no_open_items_and_panes(window.into(), &workspace, cx); -// } - -// #[gpui::test] -// async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { -// init_test_with_editor(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; -// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); -// let workspace = window.root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// select_path(&panel, "src/", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src <== selected", " > test"] -// ); -// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src", " > [EDITOR: ''] <== selected", " > test"] -// ); -// panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("test", cx)); -// assert!( -// panel.confirm(&Confirm, cx).is_none(), -// "Should not allow to confirm on conflicting new directory name" -// ) -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src", " > test"], -// "File list should be unchanged after failed folder create confirmation" -// ); - -// select_path(&panel, "src/test/", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v src", " > test <== selected"] -// ); -// panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " [EDITOR: ''] <== selected", -// " first.rs", -// " second.rs", -// " third.rs" -// ] -// ); -// panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("first.rs", cx)); -// assert!( -// panel.confirm(&Confirm, cx).is_none(), -// "Should not allow to confirm on conflicting new file name" -// ) -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs", -// " second.rs", -// " third.rs" -// ], -// "File list should be unchanged after failed file create confirmation" -// ); - -// select_path(&panel, "src/test/first.rs", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs <== selected", -// " second.rs", -// " third.rs" -// ], -// ); -// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); -// window.read_with(cx, |cx| { -// let panel = panel.read(cx); -// assert!(panel.filename_editor.is_focused(cx)); -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " [EDITOR: 'first.rs'] <== selected", -// " second.rs", -// " third.rs" -// ] -// ); -// panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("second.rs", cx)); -// assert!( -// panel.confirm(&Confirm, cx).is_none(), -// "Should not allow to confirm on conflicting file rename" -// ) -// }); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs <== selected", -// " second.rs", -// " third.rs" -// ], -// "File list should be unchanged after failed rename confirmation" -// ); -// } - -// #[gpui::test] -// async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { -// init_test_with_editor(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/src", -// json!({ -// "test": { -// "first.rs": "// First Rust file", -// "second.rs": "// Second Rust file", -// "third.rs": "// Third Rust file", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// let new_search_events_count = Arc::new(AtomicUsize::new(0)); -// let _subscription = panel.update(cx, |_, cx| { -// let subcription_count = Arc::clone(&new_search_events_count); -// cx.subscribe(&cx.handle(), move |_, _, event, _| { -// if matches!(event, Event::NewSearchInDirectory { .. }) { -// subcription_count.fetch_add(1, atomic::Ordering::SeqCst); -// } -// }) -// }); - -// toggle_expand_dir(&panel, "src/test", cx); -// select_path(&panel, "src/test/first.rs", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test", -// " first.rs <== selected", -// " second.rs", -// " third.rs" -// ] -// ); -// panel.update(cx, |panel, cx| { -// panel.new_search_in_directory(&NewSearchInDirectory, cx) -// }); -// assert_eq!( -// new_search_events_count.load(atomic::Ordering::SeqCst), -// 0, -// "Should not trigger new search in directory when called on a file" -// ); - -// select_path(&panel, "src/test", cx); -// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v src", -// " v test <== selected", -// " first.rs", -// " second.rs", -// " third.rs" -// ] -// ); -// panel.update(cx, |panel, cx| { -// panel.new_search_in_directory(&NewSearchInDirectory, cx) -// }); -// assert_eq!( -// new_search_events_count.load(atomic::Ordering::SeqCst), -// 1, -// "Should trigger new search in directory when called on a directory" -// ); -// } - -// #[gpui::test] -// async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { -// init_test_with_editor(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.insert_tree( -// "/project_root", -// json!({ -// "dir_1": { -// "nested_dir": { -// "file_a.py": "# File contents", -// "file_b.py": "# File contents", -// "file_c.py": "# File contents", -// }, -// "file_1.py": "# File contents", -// "file_2.py": "# File contents", -// "file_3.py": "# File contents", -// }, -// "dir_2": { -// "file_1.py": "# File contents", -// "file_2.py": "# File contents", -// "file_3.py": "# File contents", -// } -// }), -// ) -// .await; - -// let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// panel.update(cx, |panel, cx| { -// panel.collapse_all_entries(&CollapseAllEntries, cx) -// }); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v project_root", " > dir_1", " > dir_2",] -// ); - -// // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries -// toggle_expand_dir(&panel, "project_root/dir_1", cx); -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &[ -// "v project_root", -// " v dir_1 <== selected", -// " > nested_dir", -// " file_1.py", -// " file_2.py", -// " file_3.py", -// " > dir_2", -// ] -// ); -// } - -// #[gpui::test] -// async fn test_new_file_move(cx: &mut gpui::TestAppContext) { -// init_test(cx); - -// let fs = FakeFs::new(cx.background()); -// fs.as_fake().insert_tree("/root", json!({})).await; -// let project = Project::test(fs, ["/root".as_ref()], cx).await; -// let workspace = cx -// .add_window(|cx| Workspace::test_new(project.clone(), cx)) -// .root(cx); -// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - -// // Make a new buffer with no backing file -// workspace.update(cx, |workspace, cx| { -// Editor::new_file(workspace, &Default::default(), cx) -// }); - -// // "Save as"" the buffer, creating a new backing file for it -// let task = workspace.update(cx, |workspace, cx| { -// workspace.save_active_item(workspace::SaveIntent::Save, cx) -// }); - -// cx.foreground().run_until_parked(); -// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); -// task.await.unwrap(); - -// // Rename the file -// select_path(&panel, "root/new", cx); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v root", " new <== selected"] -// ); -// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); -// panel.update(cx, |panel, cx| { -// panel -// .filename_editor -// .update(cx, |editor, cx| editor.set_text("newer", cx)); -// }); -// panel -// .update(cx, |panel, cx| panel.confirm(&Confirm, cx)) -// .unwrap() -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v root", " newer <== selected"] -// ); - -// workspace -// .update(cx, |workspace, cx| { -// workspace.save_active_item(workspace::SaveIntent::Save, cx) -// }) -// .await -// .unwrap(); - -// cx.foreground().run_until_parked(); -// // assert that saving the file doesn't restore "new" -// assert_eq!( -// visible_entries_as_strings(&panel, 0..10, cx), -// &["v root", " newer <== selected"] -// ); -// } - -// fn toggle_expand_dir( -// panel: &View, -// path: impl AsRef, -// cx: &mut TestAppContext, -// ) { -// let path = path.as_ref(); -// panel.update(cx, |panel, cx| { -// for worktree in panel.project.read(cx).worktrees().collect::>() { -// let worktree = worktree.read(cx); -// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { -// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; -// panel.toggle_expanded(entry_id, cx); -// return; -// } -// } -// panic!("no worktree for path {:?}", path); -// }); -// } - -// fn select_path(panel: &View, path: impl AsRef, cx: &mut TestAppContext) { -// let path = path.as_ref(); -// panel.update(cx, |panel, cx| { -// for worktree in panel.project.read(cx).worktrees().collect::>() { -// let worktree = worktree.read(cx); -// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { -// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; -// panel.selection = Some(Selection { -// worktree_id: worktree.id(), -// entry_id, -// }); -// return; -// } -// } -// panic!("no worktree for path {:?}", path); -// }); -// } - -// fn visible_entries_as_strings( -// panel: &View, -// range: Range, -// cx: &mut TestAppContext, -// ) -> Vec { -// let mut result = Vec::new(); -// let mut project_entries = HashSet::new(); -// let mut has_editor = false; - -// panel.update(cx, |panel, cx| { -// panel.for_each_visible_entry(range, cx, |project_entry, details, _| { -// if details.is_editing { -// assert!(!has_editor, "duplicate editor entry"); -// has_editor = true; -// } else { -// assert!( -// project_entries.insert(project_entry), -// "duplicate project entry {:?} {:?}", -// project_entry, -// details -// ); -// } - -// let indent = " ".repeat(details.depth); -// let icon = if details.kind.is_dir() { -// if details.is_expanded { -// "v " -// } else { -// "> " -// } -// } else { -// " " -// }; -// let name = if details.is_editing { -// format!("[EDITOR: '{}']", details.filename) -// } else if details.is_processing { -// format!("[PROCESSING: '{}']", details.filename) -// } else { -// details.filename.clone() -// }; -// let selected = if details.is_selected { -// " <== selected" -// } else { -// "" -// }; -// result.push(format!("{indent}{icon}{name}{selected}")); -// }); -// }); - -// result -// } - -// fn init_test(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// init_settings(cx); -// theme::init(cx); -// language::init(cx); -// editor::init_settings(cx); -// crate::init((), cx); -// workspace::init_settings(cx); -// client::init_settings(cx); -// Project::init_settings(cx); -// }); -// } - -// fn init_test_with_editor(cx: &mut TestAppContext) { -// cx.foreground().forbid_parking(); -// cx.update(|cx| { -// let app_state = AppState::test(cx); -// theme::init(cx); -// init_settings(cx); -// language::init(cx); -// editor::init(cx); -// pane::init(cx); -// crate::init((), cx); -// workspace::init(app_state.clone(), cx); -// Project::init_settings(cx); -// }); -// } - -// fn ensure_single_file_is_opened( -// window: WindowHandle, -// expected_path: &str, -// cx: &mut TestAppContext, -// ) { -// window.update_root(cx, |workspace, cx| { -// let worktrees = workspace.worktrees(cx).collect::>(); -// assert_eq!(worktrees.len(), 1); -// let worktree_id = WorktreeId::from_usize(worktrees[0].id()); - -// let open_project_paths = workspace -// .panes() -// .iter() -// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) -// .collect::>(); -// assert_eq!( -// open_project_paths, -// vec![ProjectPath { -// worktree_id, -// path: Arc::from(Path::new(expected_path)) -// }], -// "Should have opened file, selected in project panel" -// ); -// }); -// } - -// fn submit_deletion( -// window: AnyWindowHandle, -// panel: &View, -// cx: &mut TestAppContext, -// ) { -// assert!( -// !window.has_pending_prompt(cx), -// "Should have no prompts before the deletion" -// ); -// panel.update(cx, |panel, cx| { -// panel -// .delete(&Delete, cx) -// .expect("Deletion start") -// .detach_and_log_err(cx); -// }); -// assert!( -// window.has_pending_prompt(cx), -// "Should have a prompt after the deletion" -// ); -// window.simulate_prompt_answer(0, cx); -// assert!( -// !window.has_pending_prompt(cx), -// "Should have no prompts after prompt was replied to" -// ); -// cx.foreground().run_until_parked(); -// } - -// fn ensure_no_open_items_and_panes( -// window: AnyWindowHandle, -// workspace: &View, -// cx: &mut TestAppContext, -// ) { -// assert!( -// !window.has_pending_prompt(cx), -// "Should have no prompts after deletion operation closes the file" -// ); -// window.read_with(cx, |cx| { -// let open_project_paths = workspace -// .read(cx) -// .panes() -// .iter() -// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) -// .collect::>(); -// assert!( -// open_project_paths.is_empty(), -// "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" -// ); -// }); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use gpui::{TestAppContext, View, VisualTestContext, WindowHandle}; + use pretty_assertions::assert_eq; + use project::FakeFs; + use serde_json::json; + use settings::SettingsStore; + use std::{ + collections::HashSet, + path::{Path, PathBuf}, + sync::atomic::{self, AtomicUsize}, + }; + use workspace::{pane, AppState}; + + #[gpui::test] + async fn test_visible_list(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "C": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } + } + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + toggle_expand_dir(&panel, "root1/b", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + "v root1", + " > .git", + " > a", + " v b <== selected", + " > 3", + " > 4", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + assert_eq!( + visible_entries_as_strings(&panel, 6..9, cx), + &[ + // + " > C", + " .dockerignore", + "v root2", + ] + ); + } + + #[gpui::test(iterations = 30)] + async fn test_editing_files(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "C": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } + } + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + select_path(&panel, "root1", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + // Add a file with the root folder selected. The filename editor is placed + // before the first file in the root folder. + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [EDITOR: ''] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); + panel.confirm_edit(cx).unwrap() + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [PROCESSING: 'the-new-filename'] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + " the-new-filename <== selected", + "v root2", + " > d", + " > e", + ] + ); + + select_path(&panel, "root1/b", cx); + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [EDITOR: ''] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel + .update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); + panel.confirm_edit(cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " another-filename.txt <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + select_path(&panel, "root1/b/another-filename.txt", cx); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [EDITOR: 'another-filename.txt'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + let file_name_selections = editor.selections.all::(cx); + assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); + let file_name_selection = &file_name_selections[0]; + assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); + assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); + + editor.set_text("a-different-filename.tar.gz", cx) + }); + panel.confirm_edit(cx).unwrap() + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " a-different-filename.tar.gz <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [EDITOR: 'a-different-filename.tar.gz'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + let file_name_selections = editor.selections.all::(cx); + assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); + let file_name_selection = &file_name_selections[0]; + assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); + assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot.."); + + }); + panel.cancel(&Cancel, cx) + }); + + panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > [EDITOR: ''] <== selected", + " > 3", + " > 4", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("new-dir", cx)); + panel.confirm_edit(cx).unwrap() + }); + panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > [PROCESSING: 'new-dir']", + " > 3 <== selected", + " > 4", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3 <== selected", + " > 4", + " > new-dir", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + + panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > [EDITOR: '3'] <== selected", + " > 4", + " > new-dir", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + + // Dismiss the rename editor when it loses focus. + workspace.update(cx, |_, cx| cx.blur()).unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3 <== selected", + " > 4", + " > new-dir", + " a-different-filename.tar.gz", + " > C", + " .dockerignore", + ] + ); + } + + #[gpui::test(iterations = 10)] + async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "C": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } + } + }), + ) + .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + select_path(&panel, "root1", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1 <== selected", + " > .git", + " > a", + " > b", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + // Add a file with the root folder selected. The filename editor is placed + // before the first file in the root folder. + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [EDITOR: ''] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + let confirm = panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + editor.set_text("/bdir1/dir2/the-new-filename", cx) + }); + panel.confirm_edit(cx).unwrap() + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " > C", + " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + + confirm.await.unwrap(); + assert_eq!( + visible_entries_as_strings(&panel, 0..13, cx), + &[ + "v root1", + " > .git", + " > a", + " > b", + " v bdir1", + " v dir2", + " the-new-filename <== selected", + " > C", + " .dockerignore", + "v root2", + " > d", + " > e", + ] + ); + } + + #[gpui::test] + async fn test_copy_paste(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/root1", + json!({ + "one.two.txt": "", + "one.txt": "" + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + panel.update(cx, |panel, cx| { + panel.select_next(&Default::default(), cx); + panel.select_next(&Default::default(), cx); + }); + + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + // + "v root1", + " one.two.txt <== selected", + " one.txt", + ] + ); + + // Regression test - file name is created correctly when + // the copied file's name contains multiple dots. + panel.update(cx, |panel, cx| { + panel.copy(&Default::default(), cx); + panel.paste(&Default::default(), cx); + }); + cx.executor().run_until_parked(); + + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + // + "v root1", + " one.two copy.txt", + " one.two.txt <== selected", + " one.txt", + ] + ); + + panel.update(cx, |panel, cx| { + panel.paste(&Default::default(), cx); + }); + cx.executor().run_until_parked(); + + assert_eq!( + visible_entries_as_strings(&panel, 0..50, cx), + &[ + // + "v root1", + " one.two copy 1.txt", + " one.two copy.txt", + " one.two.txt <== selected", + " one.txt", + ] + ); + } + + #[gpui::test] + async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + toggle_expand_dir(&panel, "src/test", cx); + select_path(&panel, "src/test/first.rs", cx); + panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ] + ); + ensure_single_file_is_opened(&workspace, "test/first.rs", cx); + + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " second.rs", + " third.rs" + ], + "Project panel should have no deleted file, no other file is selected in it" + ); + ensure_no_open_items_and_panes(&workspace, cx); + + select_path(&panel, "src/test/second.rs", cx); + panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " second.rs <== selected", + " third.rs" + ] + ); + ensure_single_file_is_opened(&workspace, "test/second.rs", cx); + + workspace + .update(cx, |workspace, cx| { + let active_items = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()) + .collect::>(); + assert_eq!(active_items.len(), 1); + let open_editor = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .expect("Open item should be an editor"); + open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); + }) + .unwrap(); + submit_deletion(&panel, cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v src", " v test", " third.rs"], + "Project panel should have no deleted file, with one last file remaining" + ); + ensure_no_open_items_and_panes(&workspace, cx); + } + + #[gpui::test] + async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + select_path(&panel, "src/", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + // + "v src <== selected", + " > test" + ] + ); + panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + // + "v src", + " > [EDITOR: ''] <== selected", + " > test" + ] + ); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("test", cx)); + assert!( + panel.confirm_edit(cx).is_none(), + "Should not allow to confirm on conflicting new directory name" + ) + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + // + "v src", + " > test" + ], + "File list should be unchanged after failed folder create confirmation" + ); + + select_path(&panel, "src/test/", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + // + "v src", + " > test <== selected" + ] + ); + panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " [EDITOR: ''] <== selected", + " first.rs", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("first.rs", cx)); + assert!( + panel.confirm_edit(cx).is_none(), + "Should not allow to confirm on conflicting new file name" + ) + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs", + " second.rs", + " third.rs" + ], + "File list should be unchanged after failed file create confirmation" + ); + + select_path(&panel, "src/test/first.rs", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ], + ); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " [EDITOR: 'first.rs'] <== selected", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("second.rs", cx)); + assert!( + panel.confirm_edit(cx).is_none(), + "Should not allow to confirm on conflicting file rename" + ) + }); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ], + "File list should be unchanged after failed rename confirmation" + ); + } + + #[gpui::test] + async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + let new_search_events_count = Arc::new(AtomicUsize::new(0)); + let _subscription = panel.update(cx, |_, cx| { + let subcription_count = Arc::clone(&new_search_events_count); + let view = cx.view().clone(); + cx.subscribe(&view, move |_, _, event, _| { + if matches!(event, Event::NewSearchInDirectory { .. }) { + subcription_count.fetch_add(1, atomic::Ordering::SeqCst); + } + }) + }); + + toggle_expand_dir(&panel, "src/test", cx); + select_path(&panel, "src/test/first.rs", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel.new_search_in_directory(&NewSearchInDirectory, cx) + }); + assert_eq!( + new_search_events_count.load(atomic::Ordering::SeqCst), + 0, + "Should not trigger new search in directory when called on a file" + ); + + select_path(&panel, "src/test", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test <== selected", + " first.rs", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel.new_search_in_directory(&NewSearchInDirectory, cx) + }); + assert_eq!( + new_search_events_count.load(atomic::Ordering::SeqCst), + 1, + "Should trigger new search in directory when called on a directory" + ); + } + + #[gpui::test] + async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/project_root", + json!({ + "dir_1": { + "nested_dir": { + "file_a.py": "# File contents", + "file_b.py": "# File contents", + "file_c.py": "# File contents", + }, + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + }, + "dir_2": { + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + panel.update(cx, |panel, cx| { + panel.collapse_all_entries(&CollapseAllEntries, cx) + }); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v project_root", " > dir_1", " > dir_2",] + ); + + // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries + toggle_expand_dir(&panel, "project_root/dir_1", cx); + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root", + " v dir_1 <== selected", + " > nested_dir", + " file_1.py", + " file_2.py", + " file_3.py", + " > dir_2", + ] + ); + } + + #[gpui::test] + async fn test_new_file_move(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.as_fake().insert_tree("/root", json!({})).await; + let project = Project::test(fs, ["/root".as_ref()], cx).await; + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); + + // Make a new buffer with no backing file + workspace + .update(cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .unwrap(); + + // "Save as"" the buffer, creating a new backing file for it + let save_task = workspace + .update(cx, |workspace, cx| { + workspace.save_active_item(workspace::SaveIntent::Save, cx) + }) + .unwrap(); + + cx.executor().run_until_parked(); + cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); + save_task.await.unwrap(); + + // Rename the file + select_path(&panel, "root/new", cx); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " new <== selected"] + ); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + panel.update(cx, |panel, cx| { + panel + .filename_editor + .update(cx, |editor, cx| editor.set_text("newer", cx)); + }); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + + cx.executor().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " newer <== selected"] + ); + + workspace + .update(cx, |workspace, cx| { + workspace.save_active_item(workspace::SaveIntent::Save, cx) + }) + .unwrap() + .await + .unwrap(); + + cx.executor().run_until_parked(); + // assert that saving the file doesn't restore "new" + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v root", " newer <== selected"] + ); + } + + fn toggle_expand_dir( + panel: &View, + path: impl AsRef, + cx: &mut VisualTestContext, + ) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees().collect::>() { + let worktree = worktree.read(cx); + if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { + let entry_id = worktree.entry_for_path(relative_path).unwrap().id; + panel.toggle_expanded(entry_id, cx); + return; + } + } + panic!("no worktree for path {:?}", path); + }); + } + + fn select_path(panel: &View, path: impl AsRef, cx: &mut VisualTestContext) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for worktree in panel.project.read(cx).worktrees().collect::>() { + let worktree = worktree.read(cx); + if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { + let entry_id = worktree.entry_for_path(relative_path).unwrap().id; + panel.selection = Some(Selection { + worktree_id: worktree.id(), + entry_id, + }); + return; + } + } + panic!("no worktree for path {:?}", path); + }); + } + + fn visible_entries_as_strings( + panel: &View, + range: Range, + cx: &mut VisualTestContext, + ) -> Vec { + let mut result = Vec::new(); + let mut project_entries = HashSet::new(); + let mut has_editor = false; + + panel.update(cx, |panel, cx| { + panel.for_each_visible_entry(range, cx, |project_entry, details, _| { + if details.is_editing { + assert!(!has_editor, "duplicate editor entry"); + has_editor = true; + } else { + assert!( + project_entries.insert(project_entry), + "duplicate project entry {:?} {:?}", + project_entry, + details + ); + } + + let indent = " ".repeat(details.depth); + let icon = if details.kind.is_dir() { + if details.is_expanded { + "v " + } else { + "> " + } + } else { + " " + }; + let name = if details.is_editing { + format!("[EDITOR: '{}']", details.filename) + } else if details.is_processing { + format!("[PROCESSING: '{}']", details.filename) + } else { + details.filename.clone() + }; + let selected = if details.is_selected { + " <== selected" + } else { + "" + }; + result.push(format!("{indent}{icon}{name}{selected}")); + }); + }); + + result + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + init_settings(cx); + theme::init(cx); + language::init(cx); + editor::init_settings(cx); + crate::init((), cx); + workspace::init_settings(cx); + client::init_settings(cx); + Project::init_settings(cx); + }); + } + + fn init_test_with_editor(cx: &mut TestAppContext) { + cx.update(|cx| { + let app_state = AppState::test(cx); + theme::init(cx); + init_settings(cx); + language::init(cx); + editor::init(cx); + pane::init(cx); + crate::init((), cx); + workspace::init(app_state.clone(), cx); + Project::init_settings(cx); + }); + } + + fn ensure_single_file_is_opened( + window: &WindowHandle, + expected_path: &str, + cx: &mut TestAppContext, + ) { + window + .update(cx, |workspace, cx| { + let worktrees = workspace.worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree_id = worktrees[0].read(cx).id(); + + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert_eq!( + open_project_paths, + vec![ProjectPath { + worktree_id, + path: Arc::from(Path::new(expected_path)) + }], + "Should have opened file, selected in project panel" + ); + }) + .unwrap(); + } + + fn submit_deletion(panel: &View, cx: &mut VisualTestContext) { + assert!( + !cx.has_pending_prompt(), + "Should have no prompts before the deletion" + ); + panel.update(cx, |panel, cx| panel.delete(&Delete, cx)); + assert!( + cx.has_pending_prompt(), + "Should have a prompt after the deletion" + ); + cx.simulate_prompt_answer(0); + assert!( + !cx.has_pending_prompt(), + "Should have no prompts after prompt was replied to" + ); + cx.executor().run_until_parked(); + } + + fn ensure_no_open_items_and_panes( + workspace: &WindowHandle, + cx: &mut VisualTestContext, + ) { + assert!( + !cx.has_pending_prompt(), + "Should have no prompts after deletion operation closes the file" + ); + workspace + .read_with(cx, |workspace, cx| { + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert!( + open_project_paths.is_empty(), + "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" + ); + }) + .unwrap(); + } +} diff --git a/crates/project_panel2/src/project_panel_settings.rs b/crates/project_panel2/src/project_panel_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..5b0e0194a5d5596dadadc450677895d9a91910dd --- /dev/null +++ b/crates/project_panel2/src/project_panel_settings.rs @@ -0,0 +1,45 @@ +use anyhow; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; +use settings::Settings; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProjectPanelDockPosition { + Left, + Right, +} + +#[derive(Deserialize, Debug)] +pub struct ProjectPanelSettings { + pub default_width: f32, + pub dock: ProjectPanelDockPosition, + pub file_icons: bool, + pub folder_icons: bool, + pub git_status: bool, + pub indent_size: f32, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct ProjectPanelSettingsContent { + pub default_width: Option, + pub dock: Option, + pub file_icons: Option, + pub folder_icons: Option, + pub git_status: Option, + pub indent_size: Option, +} + +impl Settings for ProjectPanelSettings { + const KEY: Option<&'static str> = Some("project_panel"); + + type FileContent = ProjectPanelSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index 9f279864ee739243c9c475395aef6d228582fe0a..2b57af0fdb11170ab4e231a0c2d7a53f7efce685 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -9,7 +9,7 @@ use schemars::{ }; use serde::Deserialize; use serde_json::Value; -use util::{asset_str, ResultExt}; +use util::asset_str; #[derive(Debug, Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] @@ -86,7 +86,9 @@ impl KeymapFile { "invalid binding value for keystroke {keystroke}, context {context:?}" ) }) - .log_err() + // todo!() + .ok() + // .log_err() .map(|action| KeyBinding::load(&keystroke, action, context.as_deref())) }) .collect::>>()?; diff --git a/crates/storybook2/src/stories/colors.rs b/crates/storybook2/src/stories/colors.rs index 13b7b36a8c67c1674f34d54a003a1b6d6788aeb9..4f8c54fa6fa67404453737f33a79113e018e346f 100644 --- a/crates/storybook2/src/stories/colors.rs +++ b/crates/storybook2/src/stories/colors.rs @@ -1,5 +1,5 @@ use crate::story::Story; -use gpui::{px, Div, Render}; +use gpui::{prelude::*, px, Div, Render}; use theme2::{default_color_scales, ColorScaleStep}; use ui::prelude::*; diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index bba798d9fe26403257d8a42fd2842ff8a89526d9..a8794afdb86053c93d06c73f82cf0af7125deeeb 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,6 +1,5 @@ use gpui::{ - actions, div, Div, FocusHandle, Focusable, FocusableKeyDispatch, KeyBinding, ParentElement, - Render, StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, + actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View, WindowContext, }; use theme2::ActiveTheme; @@ -28,7 +27,7 @@ impl FocusStory { } impl Render for FocusStory { - type Element = Div, FocusableKeyDispatch>; + type Element = Focusable>>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); @@ -42,7 +41,7 @@ impl Render for FocusStory { div() .id("parent") .focusable() - .context("parent") + .key_context("parent") .on_action(|_, action: &ActionA, cx| { println!("Action A dispatched on parent"); }) @@ -62,7 +61,7 @@ impl Render for FocusStory { .child( div() .track_focus(&self.child_1_focus) - .context("child-1") + .key_context("child-1") .on_action(|_, action: &ActionB, cx| { println!("Action B dispatched on child 1 during"); }) @@ -82,7 +81,7 @@ impl Render for FocusStory { .child( div() .track_focus(&self.child_2_focus) - .context("child-2") + .key_context("child-2") .on_action(|_, action: &ActionC, cx| { println!("Action C dispatched on child 2"); }) diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index 6831ae27220ca4691f5f8ac371c0f43e6dd7b013..507aa8db2d5db7f0d24fd33aaacc34f04a700170 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -1,5 +1,5 @@ use crate::{story::Story, story_selector::ComponentStory}; -use gpui::{Div, Render, StatefulInteractivity, View, VisualContext}; +use gpui::{prelude::*, Div, Render, Stateful, View}; use strum::IntoEnumIterator; use ui::prelude::*; @@ -12,7 +12,7 @@ impl KitchenSinkStory { } impl Render for KitchenSinkStory { - type Element = Div>; + type Element = Stateful>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let component_stories = ComponentStory::iter() diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 067c190575acebad345479b88fe6cf164c19ef81..a3f9ef5eb82a018619c52f314c00d2578d4e9b7b 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,11 +1,7 @@ -use std::sync::Arc; - use fuzzy::StringMatchCandidate; -use gpui::{ - div, Component, Div, KeyBinding, ParentElement, Render, StatelessInteractive, Styled, Task, - View, VisualContext, WindowContext, -}; +use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext}; use picker::{Picker, PickerDelegate}; +use std::sync::Arc; use theme2::ActiveTheme; pub struct PickerStory { diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index 296dc50cb491a1def608c050f65cf7ff33cedb2b..f9530269d5cdcdd73de522263003c88bd2500290 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,7 +1,4 @@ -use gpui::{ - div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteractivity, Styled, - View, VisualContext, WindowContext, -}; +use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; use theme2::ActiveTheme; pub struct ScrollStory; @@ -13,7 +10,7 @@ impl ScrollStory { } impl Render for ScrollStory { - type Element = Div>; + type Element = Stateful>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index b4a4c86e7eed15fb774ea1586e4ab36a1ea5aafe..6fc76ab9073c770dba90626611c83313c7b6445a 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,4 +1,4 @@ -use gpui::{div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext}; +use gpui::{div, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext}; pub struct TextStory; diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index 6e32eace7350414b002dfda1d3bf242665401e74..733cd6c40b6c789f484890b7a7591e5ae3cc6d40 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -35,6 +35,7 @@ pub(crate) fn one_dark() -> Theme { id: "one_dark".to_string(), name: "One Dark".into(), appearance: Appearance::Dark, + styles: ThemeStyles { system: SystemColors::default(), colors: ThemeColors { diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 8a15b52641bc30ca03e4d32c65070b89d5b8e79c..5e3329ffa149b1f744f7031162c7235961c42a96 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -19,6 +19,7 @@ const MIN_LINE_HEIGHT: f32 = 1.0; #[derive(Clone)] pub struct ThemeSettings { pub ui_font_size: Pixels, + pub ui_font: Font, pub buffer_font: Font, pub buffer_font_size: Pixels, pub buffer_line_height: BufferLineHeight, @@ -120,6 +121,12 @@ impl settings::Settings for ThemeSettings { let mut this = Self { ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(), + ui_font: Font { + family: "Helvetica".into(), + features: Default::default(), + weight: Default::default(), + style: Default::default(), + }, buffer_font: Font { family: defaults.buffer_font_family.clone().unwrap().into(), features: defaults.buffer_font_features.clone().unwrap(), diff --git a/crates/theme2/src/story.rs b/crates/theme2/src/story.rs index 8b3754b59e9799f00cc60f3cb3f45fb84fcd0cff..4296d4f99c4e2f4dfad0cbf1b60eef90efa3d0d0 100644 --- a/crates/theme2/src/story.rs +++ b/crates/theme2/src/story.rs @@ -1,4 +1,4 @@ -use gpui::{div, Component, Div, ParentElement, Styled, ViewContext}; +use gpui::{div, Component, Div, ParentComponent, Styled, ViewContext}; use crate::ActiveTheme; diff --git a/crates/theme2/src/styles/players.rs b/crates/theme2/src/styles/players.rs index 68deceb0ff88aa60bebd35ddc202b6edf0aaccc8..dfb0a6ff4eb448cf123e99c346cfe107a4b78318 100644 --- a/crates/theme2/src/styles/players.rs +++ b/crates/theme2/src/styles/players.rs @@ -143,7 +143,7 @@ use crate::{amber, blue, jade, lime, orange, pink, purple, red}; mod stories { use super::*; use crate::{ActiveTheme, Story}; - use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext}; + use gpui::{div, img, px, Div, ParentComponent, Render, Styled, ViewContext}; pub struct PlayerStory; diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 1418a977f1f1d9a0cd0085e3e42d932c4ad4ab4f..8eff92f11aac89d26020972f1586e1625b0862ed 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,11 +1,9 @@ use std::sync::Arc; -use gpui::{div, DefiniteLength, Hsla, MouseButton, WindowContext}; +use gpui::{div, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext}; -use crate::{ - h_stack, prelude::*, Icon, IconButton, IconColor, IconElement, Label, LabelColor, - LineHeightStyle, -}; +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. @@ -87,7 +85,7 @@ pub struct Button { label: SharedString, variant: ButtonVariant, width: Option, - color: Option, + color: Option, } impl Button { @@ -141,14 +139,14 @@ impl Button { self } - pub fn color(mut self, color: Option) -> Self { + pub fn color(mut self, color: Option) -> Self { self.color = color; self } - pub fn label_color(&self, color: Option) -> LabelColor { + pub fn label_color(&self, color: Option) -> TextColor { if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else if let Some(color) = color { color } else { @@ -156,21 +154,21 @@ impl Button { } } - fn render_label(&self, color: LabelColor) -> Label { + fn render_label(&self, color: TextColor) -> Label { Label::new(self.label.clone()) .color(color) .line_height_style(LineHeightStyle::UILabel) } - fn render_icon(&self, icon_color: IconColor) -> Option { + fn render_icon(&self, icon_color: TextColor) -> Option { self.icon.map(|i| IconElement::new(i).color(icon_color)) } pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let (icon_color, label_color) = match (self.disabled, self.color) { - (true, _) => (IconColor::Disabled, LabelColor::Disabled), - (_, None) => (IconColor::Default, LabelColor::Default), - (_, Some(color)) => (IconColor::from(color), color), + (true, _) => (TextColor::Disabled, TextColor::Disabled), + (_, None) => (TextColor::Default, TextColor::Default), + (_, Some(color)) => (TextColor::from(color), color), }; let mut button = h_stack() @@ -240,7 +238,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { use super::*; - use crate::{h_stack, v_stack, LabelColor, Story}; + use crate::{h_stack, v_stack, Story, TextColor}; use gpui::{rems, Div, Render}; use strum::IntoEnumIterator; @@ -265,7 +263,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label").variant(ButtonVariant::Ghost), // .state(state), @@ -276,7 +274,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -290,7 +288,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -307,7 +305,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label").variant(ButtonVariant::Filled), // .state(state), @@ -318,7 +316,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -332,7 +330,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -349,7 +347,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -363,7 +361,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -379,7 +377,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index 20dad747124ca2a652679324f04590dbf9d3f05d..5b9db177858d8f47a518766ecce925096f5177b5 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -1,12 +1,8 @@ +use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext}; use std::sync::Arc; - -use gpui::{ - div, Component, ElementId, ParentElement, StatefulInteractive, StatelessInteractive, Styled, - ViewContext, -}; use theme2::ActiveTheme; -use crate::{Icon, IconColor, IconElement, Selection}; +use crate::{Icon, IconElement, Selection, TextColor}; pub type CheckHandler = Arc) + Send + Sync>; @@ -58,9 +54,9 @@ impl Checkbox { .color( // If the checkbox is disabled we change the color of the icon. if self.disabled { - IconColor::Disabled + TextColor::Disabled } else { - IconColor::Selected + TextColor::Selected }, ), ) @@ -73,9 +69,9 @@ impl Checkbox { .color( // If the checkbox is disabled we change the color of the icon. if self.disabled { - IconColor::Disabled + TextColor::Disabled } else { - IconColor::Selected + TextColor::Selected }, ), ) diff --git a/crates/ui2/src/components/elevated_surface.rs b/crates/ui2/src/components/elevated_surface.rs index 7a6f11978e4b0dcb6eccda4678c666677c2c2981..fff232d7ed6f19739f80f9a1913e4c1d0fa69628 100644 --- a/crates/ui2/src/components/elevated_surface.rs +++ b/crates/ui2/src/components/elevated_surface.rs @@ -23,6 +23,6 @@ pub fn elevated_surface(level: ElevationIndex, cx: &mut ViewContext< .shadow(level.shadow()) } -pub fn modal(cx: &mut ViewContext) -> Div { +pub fn modal(cx: &mut ViewContext) -> Div { elevated_surface(ElevationIndex::ModalSurface, cx) } diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 75c8129608f8b1fda354461102afc5387806ce20..a0ef496d18cea94f9423ac181806dd4b5fd4a9fc 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -1,7 +1,7 @@ -use gpui::{rems, svg, Hsla}; +use gpui::{rems, svg}; use strum::EnumIter; -use crate::{prelude::*, LabelColor}; +use crate::prelude::*; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconSize { @@ -10,70 +10,6 @@ pub enum IconSize { Medium, } -#[derive(Default, PartialEq, Copy, Clone)] -pub enum IconColor { - #[default] - Default, - Accent, - Created, - Deleted, - Disabled, - Error, - Hidden, - Info, - Modified, - Muted, - Placeholder, - Player(u32), - Selected, - Success, - Warning, -} - -impl IconColor { - pub fn color(self, cx: &WindowContext) -> Hsla { - match self { - IconColor::Default => cx.theme().colors().icon, - IconColor::Muted => cx.theme().colors().icon_muted, - IconColor::Disabled => cx.theme().colors().icon_disabled, - IconColor::Placeholder => cx.theme().colors().icon_placeholder, - IconColor::Accent => cx.theme().colors().icon_accent, - IconColor::Error => cx.theme().status().error, - IconColor::Warning => cx.theme().status().warning, - IconColor::Success => cx.theme().status().success, - IconColor::Info => cx.theme().status().info, - IconColor::Selected => cx.theme().colors().icon_accent, - IconColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, - IconColor::Created => cx.theme().status().created, - IconColor::Modified => cx.theme().status().modified, - IconColor::Deleted => cx.theme().status().deleted, - IconColor::Hidden => cx.theme().status().hidden, - } - } -} - -impl From for IconColor { - fn from(label: LabelColor) -> Self { - match label { - LabelColor::Default => IconColor::Default, - LabelColor::Muted => IconColor::Muted, - LabelColor::Disabled => IconColor::Disabled, - LabelColor::Placeholder => IconColor::Placeholder, - LabelColor::Accent => IconColor::Accent, - LabelColor::Error => IconColor::Error, - LabelColor::Warning => IconColor::Warning, - LabelColor::Success => IconColor::Success, - LabelColor::Info => IconColor::Info, - LabelColor::Selected => IconColor::Selected, - LabelColor::Player(i) => IconColor::Player(i), - LabelColor::Created => IconColor::Created, - LabelColor::Modified => IconColor::Modified, - LabelColor::Deleted => IconColor::Deleted, - LabelColor::Hidden => IconColor::Hidden, - } - } -} - #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum Icon { Ai, @@ -193,21 +129,29 @@ impl Icon { #[derive(Component)] pub struct IconElement { - icon: Icon, - color: IconColor, + path: SharedString, + color: TextColor, size: IconSize, } impl IconElement { pub fn new(icon: Icon) -> Self { Self { - icon, - color: IconColor::default(), + path: icon.path().into(), + color: TextColor::default(), + size: IconSize::default(), + } + } + + pub fn from_path(path: impl Into) -> Self { + Self { + path: path.into(), + color: TextColor::default(), size: IconSize::default(), } } - pub fn color(mut self, color: IconColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -226,7 +170,7 @@ impl IconElement { svg() .size(svg_size) .flex_none() - .path(self.icon.path()) + .path(self.path) .text_color(self.color.color(cx)) } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index b20cd3103611cd5a43963496981305bb69bb7ace..7afaa1224343cc872ac8aac0cc6f8a0b5a245198 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, IconColor, IconElement, TextTooltip}; -use gpui::{MouseButton, VisualContext}; +use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextTooltip}; +use gpui::{prelude::*, MouseButton, VisualContext}; use std::sync::Arc; struct IconButtonHandlers { @@ -16,7 +16,7 @@ impl Default for IconButtonHandlers { pub struct IconButton { id: ElementId, icon: Icon, - color: IconColor, + color: TextColor, variant: ButtonVariant, state: InteractionState, tooltip: Option, @@ -28,7 +28,7 @@ impl IconButton { Self { id: id.into(), icon, - color: IconColor::default(), + color: TextColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), tooltip: None, @@ -41,7 +41,7 @@ impl IconButton { self } - pub fn color(mut self, color: IconColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -71,7 +71,7 @@ impl IconButton { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { - (InteractionState::Disabled, _) => IconColor::Disabled, + (InteractionState::Disabled, _) => TextColor::Disabled, _ => self.color, }; diff --git a/crates/ui2/src/components/input.rs b/crates/ui2/src/components/input.rs index 1a44827fe8d308f2f73dd3609bf9a4a5ab7625cb..42de03db126f6b4d1060f739e350fae7c480b48f 100644 --- a/crates/ui2/src/components/input.rs +++ b/crates/ui2/src/components/input.rs @@ -1,6 +1,5 @@ -use crate::prelude::*; -use crate::Label; -use crate::LabelColor; +use crate::{prelude::*, Label}; +use gpui::prelude::*; #[derive(Default, PartialEq)] pub enum InputVariant { @@ -71,15 +70,15 @@ impl Input { }; let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else { - LabelColor::Placeholder + TextColor::Placeholder }); let label = Label::new(self.value.clone()).color(if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else { - LabelColor::Default + TextColor::Default }); div() diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index a3e5a870a6b61cad638a1a8b174c364273d559f2..04e036f3655c50f2110a5675fa75aa131f139de8 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -3,7 +3,7 @@ use strum::EnumIter; use crate::prelude::*; -#[derive(Component)] +#[derive(Component, Clone)] pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 4b9cea8dc2c70676dcdf218d50b1f412cca7f54e..cbb75278c29487e0b5466421ce33f52342e60fed 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -3,8 +3,15 @@ use gpui::{relative, Hsla, Text, TextRun, WindowContext}; use crate::prelude::*; use crate::styled_ext::StyledExt; +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] +pub enum LabelSize { + #[default] + Default, + Small, +} + #[derive(Default, PartialEq, Copy, Clone)] -pub enum LabelColor { +pub enum TextColor { #[default] Default, Accent, @@ -23,24 +30,24 @@ pub enum LabelColor { Warning, } -impl LabelColor { - pub fn hsla(&self, cx: &WindowContext) -> Hsla { +impl TextColor { + pub fn color(&self, cx: &WindowContext) -> Hsla { match self { - LabelColor::Default => cx.theme().colors().text, - LabelColor::Muted => cx.theme().colors().text_muted, - LabelColor::Created => cx.theme().status().created, - LabelColor::Modified => cx.theme().status().modified, - LabelColor::Deleted => cx.theme().status().deleted, - LabelColor::Disabled => cx.theme().colors().text_disabled, - LabelColor::Hidden => cx.theme().status().hidden, - LabelColor::Info => cx.theme().status().info, - LabelColor::Placeholder => cx.theme().colors().text_placeholder, - LabelColor::Accent => cx.theme().colors().text_accent, - LabelColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, - LabelColor::Error => cx.theme().status().error, - LabelColor::Selected => cx.theme().colors().text_accent, - LabelColor::Success => cx.theme().status().success, - LabelColor::Warning => cx.theme().status().warning, + TextColor::Default => cx.theme().colors().text, + TextColor::Muted => cx.theme().colors().text_muted, + TextColor::Created => cx.theme().status().created, + TextColor::Modified => cx.theme().status().modified, + TextColor::Deleted => cx.theme().status().deleted, + TextColor::Disabled => cx.theme().colors().text_disabled, + TextColor::Hidden => cx.theme().status().hidden, + TextColor::Info => cx.theme().status().info, + TextColor::Placeholder => cx.theme().colors().text_placeholder, + TextColor::Accent => cx.theme().colors().text_accent, + TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, + TextColor::Error => cx.theme().status().error, + TextColor::Selected => cx.theme().colors().text_accent, + TextColor::Success => cx.theme().status().success, + TextColor::Warning => cx.theme().status().warning, } } } @@ -56,8 +63,9 @@ pub enum LineHeightStyle { #[derive(Component)] pub struct Label { label: SharedString, + size: LabelSize, line_height_style: LineHeightStyle, - color: LabelColor, + color: TextColor, strikethrough: bool, } @@ -65,13 +73,19 @@ impl Label { pub fn new(label: impl Into) -> Self { Self { label: label.into(), + size: LabelSize::Default, line_height_style: LineHeightStyle::default(), - color: LabelColor::Default, + color: TextColor::Default, strikethrough: false, } } - pub fn color(mut self, color: LabelColor) -> Self { + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -95,14 +109,17 @@ impl Label { .top_1_2() .w_full() .h_px() - .bg(LabelColor::Hidden.hsla(cx)), + .bg(TextColor::Hidden.color(cx)), ) }) - .text_ui() + .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.hsla(cx)) + .text_color(self.color.color(cx)) .child(self.label.clone()) } } @@ -110,7 +127,8 @@ impl Label { #[derive(Component)] pub struct HighlightedLabel { label: SharedString, - color: LabelColor, + size: LabelSize, + color: TextColor, highlight_indices: Vec, strikethrough: bool, } @@ -121,13 +139,19 @@ impl HighlightedLabel { pub fn new(label: impl Into, highlight_indices: Vec) -> Self { Self { label: label.into(), - color: LabelColor::Default, + size: LabelSize::Default, + color: TextColor::Default, highlight_indices, strikethrough: false, } } - pub fn color(mut self, color: LabelColor) -> Self { + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -146,7 +170,7 @@ impl HighlightedLabel { let mut runs: Vec = Vec::new(); for (char_ix, char) in self.label.char_indices() { - let mut color = self.color.hsla(cx); + let mut color = self.color.color(cx); if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { @@ -183,9 +207,13 @@ impl HighlightedLabel { .my_auto() .w_full() .h_px() - .bg(LabelColor::Hidden.hsla(cx)), + .bg(TextColor::Hidden.color(cx)), ) }) + .map(|this| match self.size { + LabelSize::Default => this.text_ui(), + LabelSize::Small => this.text_ui_sm(), + }) .child(Text::styled(self.label, runs)) } } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 5c42975b17c2d02c456aa1b435766b8b50812ceb..1ddad269dd752fbfce95028c11fcc555f7bbfeb0 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,11 +1,11 @@ use gpui::div; +use crate::prelude::*; use crate::settings::user_settings; use crate::{ - disclosure_control, h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, - LabelColor, Toggle, + disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label, + TextColor, Toggle, }; -use crate::{prelude::*, GraphicSlot}; #[derive(Clone, Copy, Default, Debug, PartialEq)] pub enum ListItemVariant { @@ -68,7 +68,7 @@ impl ListHeader { .items_center() .children(icons.into_iter().map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })), ), @@ -106,10 +106,10 @@ impl ListHeader { .items_center() .children(self.left_icon.map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })) - .child(Label::new(self.label.clone()).color(LabelColor::Muted)), + .child(Label::new(self.label.clone()).color(TextColor::Muted)), ) .child(disclosure_control), ) @@ -157,10 +157,10 @@ impl ListSubHeader { .items_center() .children(self.left_icon.map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })) - .child(Label::new(self.label.clone()).color(LabelColor::Muted)), + .child(Label::new(self.label.clone()).color(TextColor::Muted)), ), ) } @@ -291,7 +291,7 @@ impl ListEntry { h_stack().child( IconElement::new(i) .size(IconSize::Small) - .color(IconColor::Muted), + .color(TextColor::Muted), ), ), Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))), @@ -394,7 +394,7 @@ impl List { (false, _) => div().children(self.items), (true, Toggle::Toggled(false)) => div(), (true, _) => { - div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted)) + div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted)) } }; diff --git a/crates/ui2/src/components/modal.rs b/crates/ui2/src/components/modal.rs index 75528b5c3460defeb139f7a16d8d311d87a5093b..c3d71a78d8dd873d1d898da43edb69fadca66188 100644 --- a/crates/ui2/src/components/modal.rs +++ b/crates/ui2/src/components/modal.rs @@ -74,7 +74,7 @@ impl Modal { } } -impl ParentElement for Modal { +impl ParentComponent for Modal { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 4e1034595db8043bae80823fb793509c86f01ff0..5adf794a5efcc6435a7f302fe94f0c59bf530b63 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -1,5 +1,5 @@ -use crate::prelude::*; -use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor}; +use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label}; +use gpui::prelude::*; #[derive(Component)] pub struct Palette { @@ -54,7 +54,7 @@ impl Palette { 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(LabelColor::Placeholder), + Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder), ))) .child( div() @@ -75,7 +75,7 @@ impl Palette { Some( h_stack().justify_between().px_2().py_1().child( Label::new(self.empty_string.clone()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) } else { @@ -108,7 +108,7 @@ impl Palette { pub struct PaletteItem { pub label: SharedString, pub sublabel: Option, - pub keybinding: Option, + pub key_binding: Option, } impl PaletteItem { @@ -116,7 +116,7 @@ impl PaletteItem { Self { label: label.into(), sublabel: None, - keybinding: None, + key_binding: None, } } @@ -130,11 +130,8 @@ impl PaletteItem { self } - pub fn keybinding(mut self, keybinding: K) -> Self - where - K: Into>, - { - self.keybinding = keybinding.into(); + pub fn key_binding(mut self, key_binding: impl Into>) -> Self { + self.key_binding = key_binding.into(); self } @@ -149,7 +146,7 @@ impl PaletteItem { .child(Label::new(self.label.clone())) .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))), ) - .children(self.keybinding) + .children(self.key_binding) } } @@ -182,23 +179,23 @@ mod stories { .placeholder("Execute a command...") .items(vec![ PaletteItem::new("theme selector: toggle") - .keybinding(KeyBinding::new(binding("cmd-k cmd-t"))), + .key_binding(KeyBinding::new(binding("cmd-k cmd-t"))), PaletteItem::new("assistant: inline assist") - .keybinding(KeyBinding::new(binding("cmd-enter"))), + .key_binding(KeyBinding::new(binding("cmd-enter"))), PaletteItem::new("assistant: quote selection") - .keybinding(KeyBinding::new(binding("cmd-<"))), + .key_binding(KeyBinding::new(binding("cmd-<"))), PaletteItem::new("assistant: toggle focus") - .keybinding(KeyBinding::new(binding("cmd-?"))), + .key_binding(KeyBinding::new(binding("cmd-?"))), PaletteItem::new("auto update: check"), PaletteItem::new("auto update: view release notes"), PaletteItem::new("branches: open recent") - .keybinding(KeyBinding::new(binding("cmd-alt-b"))), + .key_binding(KeyBinding::new(binding("cmd-alt-b"))), PaletteItem::new("chat panel: toggle focus"), PaletteItem::new("cli: install"), PaletteItem::new("client: sign in"), PaletteItem::new("client: sign out"), PaletteItem::new("editor: cancel") - .keybinding(KeyBinding::new(binding("escape"))), + .key_binding(KeyBinding::new(binding("escape"))), ]), ) } diff --git a/crates/ui2/src/components/panel.rs b/crates/ui2/src/components/panel.rs index 1762003a2c2f8b6a8569a5ffd99770d90aef130d..d9fc50dd923cc00084f257387efcbb1705efa9f1 100644 --- a/crates/ui2/src/components/panel.rs +++ b/crates/ui2/src/components/panel.rs @@ -1,4 +1,4 @@ -use gpui::{AbsoluteLength, AnyElement}; +use gpui::{prelude::*, AbsoluteLength, AnyElement}; use smallvec::SmallVec; use crate::prelude::*; @@ -113,7 +113,7 @@ impl Panel { } } -impl ParentElement for Panel { +impl ParentComponent for Panel { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } @@ -126,7 +126,7 @@ pub use stories::*; mod stories { use super::*; use crate::{Label, Story}; - use gpui::{Div, Render}; + use gpui::{Div, InteractiveComponent, Render}; pub struct PanelStory; diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index e936dc924a5fcba43e7d0905d5fac455d9ebd5c8..820fe5b361c93560dc7c208313ac80f7b23c069f 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -1,6 +1,6 @@ use crate::prelude::*; -use crate::{Icon, IconColor, IconElement, Label, LabelColor}; -use gpui::{red, Div, ElementId, Render, View, VisualContext}; +use crate::{Icon, IconElement, Label, TextColor}; +use gpui::{prelude::*, red, Div, ElementId, Render, View}; #[derive(Component, Clone)] pub struct Tab { @@ -92,20 +92,18 @@ impl Tab { let label = match (self.git_status, is_deleted) { (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone()) - .color(LabelColor::Hidden) + .color(TextColor::Hidden) .set_strikethrough(true), (GitStatus::None, false) => Label::new(self.title.clone()), - (GitStatus::Created, false) => { - Label::new(self.title.clone()).color(LabelColor::Created) - } + (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created), (GitStatus::Modified, false) => { - Label::new(self.title.clone()).color(LabelColor::Modified) + Label::new(self.title.clone()).color(TextColor::Modified) } - (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent), + (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent), (GitStatus::Conflict, false) => Label::new(self.title.clone()), }; - let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); + let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted); let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current { false => ( @@ -148,7 +146,7 @@ impl Tab { .children(has_fs_conflict.then(|| { IconElement::new(Icon::ExclamationTriangle) .size(crate::IconSize::Small) - .color(IconColor::Warning) + .color(TextColor::Warning) })) .children(self.icon.map(IconElement::new)) .children(if self.close_side == IconSide::Left { diff --git a/crates/ui2/src/components/toast.rs b/crates/ui2/src/components/toast.rs index 4164be2c3e71fdf22b0b9a852588b796d0d1de21..0fcfe6038b97e3b6ba8cc3f88db8cd12f8b2bfc0 100644 --- a/crates/ui2/src/components/toast.rs +++ b/crates/ui2/src/components/toast.rs @@ -1,7 +1,6 @@ -use gpui::AnyElement; -use smallvec::SmallVec; - use crate::prelude::*; +use gpui::{prelude::*, AnyElement}; +use smallvec::SmallVec; #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] pub enum ToastOrigin { @@ -59,7 +58,7 @@ impl Toast { } } -impl ParentElement for Toast { +impl ParentComponent for Toast { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } diff --git a/crates/ui2/src/components/toggle.rs b/crates/ui2/src/components/toggle.rs index 368c95662fa51ed804c3d73f0d456d4f071a76d1..8388e2753108c5f3cbec688dbae1ea61f985a597 100644 --- a/crates/ui2/src/components/toggle.rs +++ b/crates/ui2/src/components/toggle.rs @@ -1,6 +1,6 @@ -use gpui::{div, Component, ParentElement}; +use gpui::{div, Component, ParentComponent}; -use crate::{Icon, IconColor, IconElement, IconSize}; +use crate::{Icon, IconElement, IconSize, TextColor}; /// Whether the entry is toggleable, and if so, whether it is currently toggled. /// @@ -49,12 +49,12 @@ pub fn disclosure_control(toggle: Toggle) -> impl Component { (false, _) => div(), (_, true) => div().child( IconElement::new(Icon::ChevronDown) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small), ), (_, false) => div().child( IconElement::new(Icon::ChevronRight) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small), ), } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index ee3e9708c0af88cc53e52ddca49beb98810332d8..8463ed7ba4a34a1ccb110499db8dbe0c61d0f9b8 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,32 +1,61 @@ -use gpui::{div, Div, ParentElement, Render, SharedString, Styled, ViewContext}; -use theme2::ActiveTheme; +use gpui::{Div, Render}; +use settings2::Settings; +use theme2::{ActiveTheme, ThemeSettings}; -use crate::StyledExt; +use crate::prelude::*; +use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor}; -#[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, + meta: Option, + key_binding: Option, } impl TextTooltip { pub fn new(title: impl Into) -> Self { Self { title: title.into(), + meta: None, + key_binding: None, } } + + pub fn meta(mut self, meta: impl Into) -> Self { + self.meta = Some(meta.into()); + self + } + + pub fn key_binding(mut self, key_binding: impl Into>) -> Self { + self.key_binding = key_binding.into(); + self + } } impl Render for TextTooltip { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + v_stack() .elevation_2(cx) - .font("Zed Sans") - .text_ui() + .font(ui_font) + .text_ui_sm() .text_color(cx.theme().colors().text) .py_1() .px_2() - .child(self.title.clone()) + .child( + h_stack() + .child(self.title.clone()) + .when_some(self.key_binding.clone(), |this, key_binding| { + this.justify_between().child(key_binding) + }), + ) + .when_some(self.meta.clone(), |this, meta| { + this.child( + Label::new(meta) + .size(LabelSize::Small) + .color(TextColor::Muted), + ) + }) } } diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 545f437a9b4de2ee1deb14eada747e4315112557..d4abb78c21c701dae15e5b35264f50cde3e4c438 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -1,13 +1,13 @@ use gpui::rems; use gpui::Rems; pub use gpui::{ - div, Component, Element, ElementId, ParentElement, SharedString, StatefulInteractive, - StatelessInteractive, Styled, ViewContext, WindowContext, + div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString, + Styled, ViewContext, WindowContext, }; pub use crate::elevation::*; -pub use crate::ButtonVariant; pub use crate::StyledExt; +pub use crate::{ButtonVariant, TextColor}; pub use theme2::ActiveTheme; use gpui::Hsla; diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 89aef8140a062685b59f00c508d99fea5fe19b1b..bb81d6230fdc7a68f9e2b2eae826a59b6b57297b 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -10,9 +10,9 @@ use theme2::ActiveTheme; use crate::{binding, HighlightedText}; use crate::{ Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, - HighlightedLine, Icon, KeyBinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, - MicStatus, Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, - PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, + HighlightedLine, Icon, KeyBinding, Label, ListEntry, ListEntrySize, Livestream, MicStatus, + Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, PublicPlayer, + ScreenShareStatus, Symbol, Tab, TextColor, Toggle, VideoStatus, }; use crate::{ListItem, NotificationAction}; @@ -490,20 +490,20 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new(".config")) .left_icon(Icon::Folder.into()) .indent_level(1), - ListEntry::new(Label::new(".git").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".git").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new(".cargo")) .left_icon(Icon::Folder.into()) .indent_level(1), - ListEntry::new(Label::new(".idea").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".idea").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new("assets")) .left_icon(Icon::Folder.into()) .indent_level(1) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden)) + ListEntry::new(Label::new("cargo-target").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new("crates")) @@ -528,7 +528,7 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new("call")) .left_icon(Icon::Folder.into()) .indent_level(2), - ListEntry::new(Label::new("sqlez").color(LabelColor::Modified)) + ListEntry::new(Label::new("sqlez").color(TextColor::Modified)) .left_icon(Icon::Folder.into()) .indent_level(2) .toggle(Toggle::Toggled(false)), @@ -543,45 +543,45 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new("derive_element.rs")) .left_icon(Icon::FileRust.into()) .indent_level(4), - ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) + ListEntry::new(Label::new("storybook").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(1) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("docs").color(LabelColor::Default)) + ListEntry::new(Label::new("docs").color(TextColor::Default)) .left_icon(Icon::Folder.into()) .indent_level(2) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("src").color(LabelColor::Modified)) + ListEntry::new(Label::new("src").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(3) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("ui").color(LabelColor::Modified)) + ListEntry::new(Label::new("ui").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(4) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("component").color(LabelColor::Created)) + ListEntry::new(Label::new("component").color(TextColor::Created)) .left_icon(Icon::FolderOpen.into()) .indent_level(5) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("facepile.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("follow_group.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created)) + ListEntry::new(Label::new("list_item.rs").color(TextColor::Created)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("tab.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("tab.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("target").color(LabelColor::Hidden)) + ListEntry::new(Label::new("target").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new(".dockerignore")) .left_icon(Icon::FileGeneric.into()) .indent_level(1), - ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".DS_Store").color(TextColor::Hidden)) .left_icon(Icon::FileGeneric.into()) .indent_level(1), ListEntry::new(Label::new("Cargo.lock")) @@ -701,16 +701,16 @@ pub fn static_collab_panel_channels() -> Vec { pub fn example_editor_actions() -> Vec { vec![ - PaletteItem::new("New File").keybinding(KeyBinding::new(binding("cmd-n"))), - PaletteItem::new("Open File").keybinding(KeyBinding::new(binding("cmd-o"))), - PaletteItem::new("Save File").keybinding(KeyBinding::new(binding("cmd-s"))), - PaletteItem::new("Cut").keybinding(KeyBinding::new(binding("cmd-x"))), - PaletteItem::new("Copy").keybinding(KeyBinding::new(binding("cmd-c"))), - PaletteItem::new("Paste").keybinding(KeyBinding::new(binding("cmd-v"))), - PaletteItem::new("Undo").keybinding(KeyBinding::new(binding("cmd-z"))), - PaletteItem::new("Redo").keybinding(KeyBinding::new(binding("cmd-shift-z"))), - PaletteItem::new("Find").keybinding(KeyBinding::new(binding("cmd-f"))), - PaletteItem::new("Replace").keybinding(KeyBinding::new(binding("cmd-r"))), + PaletteItem::new("New File").key_binding(KeyBinding::new(binding("cmd-n"))), + PaletteItem::new("Open File").key_binding(KeyBinding::new(binding("cmd-o"))), + PaletteItem::new("Save File").key_binding(KeyBinding::new(binding("cmd-s"))), + PaletteItem::new("Cut").key_binding(KeyBinding::new(binding("cmd-x"))), + PaletteItem::new("Copy").key_binding(KeyBinding::new(binding("cmd-c"))), + PaletteItem::new("Paste").key_binding(KeyBinding::new(binding("cmd-v"))), + PaletteItem::new("Undo").key_binding(KeyBinding::new(binding("cmd-z"))), + PaletteItem::new("Redo").key_binding(KeyBinding::new(binding("cmd-shift-z"))), + PaletteItem::new("Find").key_binding(KeyBinding::new(binding("cmd-f"))), + PaletteItem::new("Replace").key_binding(KeyBinding::new(binding("cmd-r"))), PaletteItem::new("Jump to Line"), PaletteItem::new("Select All"), PaletteItem::new("Deselect All"), diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index 0407d98f862abc37d319bb0ad61204a43ff3a451..d9911e683358dfd37a9110f71cf3899debe52904 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -1,4 +1,4 @@ -use gpui::{Div, ElementInteractivity, KeyDispatch, Styled, UniformList, ViewContext}; +use gpui::{Styled, ViewContext}; use theme2::ActiveTheme; use crate::{ElevationIndex, UITextSize}; @@ -93,11 +93,4 @@ pub trait StyledExt: Styled + Sized { } } -impl StyledExt for Div -where - I: ElementInteractivity, - F: KeyDispatch, -{ -} - -impl StyledExt for UniformList {} +impl StyledExt for E {} diff --git a/crates/ui2/src/to_extract/assistant_panel.rs b/crates/ui2/src/to_extract/assistant_panel.rs index 8a35757f5c484315daebea12c0af7186c418f96e..f111dad83024f539d033701bfab66a7c10dbb446 100644 --- a/crates/ui2/src/to_extract/assistant_panel.rs +++ b/crates/ui2/src/to_extract/assistant_panel.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::{Icon, IconButton, Label, Panel, PanelSide}; -use gpui::{rems, AbsoluteLength}; +use gpui::{prelude::*, rems, AbsoluteLength}; #[derive(Component)] pub struct AssistantPanel { diff --git a/crates/ui2/src/to_extract/breadcrumb.rs b/crates/ui2/src/to_extract/breadcrumb.rs index 782f772fa1472fe6dd213da2d3e7346a03b84395..fd43a5b3bf58556ca003e00114b1f060f82ba453 100644 --- a/crates/ui2/src/to_extract/breadcrumb.rs +++ b/crates/ui2/src/to_extract/breadcrumb.rs @@ -1,9 +1,7 @@ +use crate::{h_stack, prelude::*, HighlightedText}; +use gpui::{prelude::*, Div}; use std::path::PathBuf; -use crate::prelude::*; -use crate::{h_stack, HighlightedText}; -use gpui::Div; - #[derive(Clone)] pub struct Symbol(pub Vec); diff --git a/crates/ui2/src/to_extract/buffer_search.rs b/crates/ui2/src/to_extract/buffer_search.rs index 9993cd361294067112c6b629ef9f5e2698e4e3cb..996ac6d2531306d444f81c8d67e7451db4a9e312 100644 --- a/crates/ui2/src/to_extract/buffer_search.rs +++ b/crates/ui2/src/to_extract/buffer_search.rs @@ -1,7 +1,7 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; -use crate::{h_stack, Icon, IconButton, IconColor, Input}; +use crate::{h_stack, Icon, IconButton, Input, TextColor}; #[derive(Clone)] pub struct BufferSearch { @@ -36,7 +36,7 @@ impl Render for BufferSearch { .child( h_stack().child(Input::new("Search")).child( IconButton::::new("replace", Icon::Replace) - .when(self.is_replace_open, |this| this.color(IconColor::Accent)) + .when(self.is_replace_open, |this| this.color(TextColor::Accent)) .on_click(|buffer_search, cx| { buffer_search.toggle_replace(cx); }), diff --git a/crates/ui2/src/to_extract/chat_panel.rs b/crates/ui2/src/to_extract/chat_panel.rs index 538b5dfceba5bb753feb0504b1eb880140aef291..7e2846a3f6b66f8079672a8edab09dd00c19f120 100644 --- a/crates/ui2/src/to_extract/chat_panel.rs +++ b/crates/ui2/src/to_extract/chat_panel.rs @@ -1,7 +1,6 @@ +use crate::{prelude::*, Icon, IconButton, Input, Label}; use chrono::NaiveDateTime; - -use crate::prelude::*; -use crate::{Icon, IconButton, Input, Label, LabelColor}; +use gpui::prelude::*; #[derive(Component)] pub struct ChatPanel { @@ -95,7 +94,7 @@ impl ChatMessage { .child(Label::new(self.author.clone())) .child( Label::new(self.sent_at.format("%m/%d/%Y").to_string()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) .child(div().child(Label::new(self.text.clone()))) diff --git a/crates/ui2/src/to_extract/collab_panel.rs b/crates/ui2/src/to_extract/collab_panel.rs index d56166ad2e25303f1d0d8e01d50fe4e233437cf8..256a648c0d42e39cfaccc9c4115d0ff431df8c85 100644 --- a/crates/ui2/src/to_extract/collab_panel.rs +++ b/crates/ui2/src/to_extract/collab_panel.rs @@ -1,7 +1,8 @@ -use crate::{prelude::*, Toggle}; use crate::{ - static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader, + prelude::*, static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, + List, ListHeader, Toggle, }; +use gpui::prelude::*; #[derive(Component)] pub struct CollabPanel { diff --git a/crates/ui2/src/to_extract/copilot.rs b/crates/ui2/src/to_extract/copilot.rs index 8750ab3c51a28cd1f91f67f09c4cd7d419a7b98b..c5622f5be6ddcbfe735b6befa4abd8d8ea47ae95 100644 --- a/crates/ui2/src/to_extract/copilot.rs +++ b/crates/ui2/src/to_extract/copilot.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, Button, Label, LabelColor, Modal}; +use crate::{prelude::*, Button, Label, Modal, TextColor}; #[derive(Component)] pub struct CopilotModal { @@ -14,7 +14,7 @@ impl CopilotModal { 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(LabelColor::Muted)) + .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)), ) } diff --git a/crates/ui2/src/to_extract/editor_pane.rs b/crates/ui2/src/to_extract/editor_pane.rs index fd21e81242c34237f9d16fb63f66312ebc678fbf..bd34c22805a972fca02c62a3996778c144feebe2 100644 --- a/crates/ui2/src/to_extract/editor_pane.rs +++ b/crates/ui2/src/to_extract/editor_pane.rs @@ -5,7 +5,7 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::{ hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon, - IconButton, IconColor, Symbol, Tab, TabBar, Toolbar, + IconButton, Symbol, Tab, TabBar, TextColor, Toolbar, }; #[derive(Clone)] @@ -60,12 +60,12 @@ impl Render for EditorPane { Toolbar::new() .left_item(Breadcrumb::new(self.path.clone(), self.symbols.clone())) .right_items(vec![ - IconButton::new("toggle_inlay_hints", Icon::InlayHint), + IconButton::::new("toggle_inlay_hints", Icon::InlayHint), IconButton::::new("buffer_search", Icon::MagnifyingGlass) .when(self.is_buffer_search_open, |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) - .on_click(|editor, cx| { + .on_click(|editor: &mut Self, cx| { editor.toggle_buffer_search(cx); }), IconButton::new("inline_assist", Icon::MagicWand), diff --git a/crates/ui2/src/to_extract/notifications_panel.rs b/crates/ui2/src/to_extract/notifications_panel.rs index b2cc4a7846c00acf32b4afb1d65d196f8caeda8a..f56194fc473d90507fba44b1cb3814b3f1b814b7 100644 --- a/crates/ui2/src/to_extract/notifications_panel.rs +++ b/crates/ui2/src/to_extract/notifications_panel.rs @@ -1,10 +1,9 @@ -use crate::utils::naive_format_distance_from_now; use crate::{ - h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, ButtonOrIconButton, - Icon, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, - PublicPlayer, UnreadIndicator, + h_stack, prelude::*, static_new_notification_items_2, utils::naive_format_distance_from_now, + v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LineHeightStyle, + ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, TextColor, UnreadIndicator, }; -use crate::{ClickHandler, ListHeader}; +use gpui::prelude::*; #[derive(Component)] pub struct NotificationsPanel { @@ -48,7 +47,7 @@ impl NotificationsPanel { .border_color(cx.theme().colors().border_variant) .child( Label::new("Search...") - .color(LabelColor::Placeholder) + .color(TextColor::Placeholder) .line_height_style(LineHeightStyle::UILabel), ), ) @@ -252,7 +251,7 @@ impl Notification { if let Some(icon) = icon { meta_el = meta_el.child(IconElement::new(icon.clone())); } - meta_el.child(Label::new(text.clone()).color(LabelColor::Muted)) + meta_el.child(Label::new(text.clone()).color(TextColor::Muted)) }) .collect::>(), ) @@ -311,7 +310,7 @@ impl Notification { true, true, )) - .color(LabelColor::Muted), + .color(TextColor::Muted), ) .child(self.render_meta_items(cx)), ) @@ -321,11 +320,11 @@ impl Notification { // Show the taken_message (Some(_), Some(action_taken)) => h_stack() .children(action_taken.taken_message.0.map(|icon| { - IconElement::new(icon).color(crate::IconColor::Muted) + IconElement::new(icon).color(crate::TextColor::Muted) })) .child( Label::new(action_taken.taken_message.1.clone()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), // Show the actions (Some(actions), None) => { diff --git a/crates/ui2/src/to_extract/panes.rs b/crates/ui2/src/to_extract/panes.rs index b57b77d5eeb8ec71dc4ab075d63269fcac158cbe..288419d8bf8ebdb1af71b668684ebf38619ce73b 100644 --- a/crates/ui2/src/to_extract/panes.rs +++ b/crates/ui2/src/to_extract/panes.rs @@ -59,7 +59,7 @@ impl Pane { } } -impl ParentElement for Pane { +impl ParentComponent for Pane { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } diff --git a/crates/ui2/src/to_extract/project_panel.rs b/crates/ui2/src/to_extract/project_panel.rs index d4f5c724265f549eaed54040364abf655c6a5bfd..018f9a4bf10154504df580a1c95e31fe0a4a1016 100644 --- a/crates/ui2/src/to_extract/project_panel.rs +++ b/crates/ui2/src/to_extract/project_panel.rs @@ -1,7 +1,8 @@ -use crate::prelude::*; use crate::{ - static_project_panel_project_items, static_project_panel_single_items, Input, List, ListHeader, + prelude::*, static_project_panel_project_items, static_project_panel_single_items, Input, List, + ListHeader, }; +use gpui::prelude::*; #[derive(Component)] pub struct ProjectPanel { diff --git a/crates/ui2/src/to_extract/status_bar.rs b/crates/ui2/src/to_extract/status_bar.rs index 34a5993e69da552d9045a7ab23c5fa30d3d218e3..eee96ecad85f0b39ecda110fe3df86340190b155 100644 --- a/crates/ui2/src/to_extract/status_bar.rs +++ b/crates/ui2/src/to_extract/status_bar.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::prelude::*; -use crate::{Button, Icon, IconButton, IconColor, ToolDivider, Workspace}; +use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace}; #[derive(Default, PartialEq)] pub enum Tool { @@ -110,18 +110,18 @@ impl StatusBar { .child( IconButton::::new("project_panel", Icon::FileTree) .when(workspace.is_project_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) - .on_click(|workspace, cx| { + .on_click(|workspace: &mut Workspace, cx| { workspace.toggle_project_panel(cx); }), ) .child( IconButton::::new("collab_panel", Icon::Hash) .when(workspace.is_collab_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) - .on_click(|workspace, cx| { + .on_click(|workspace: &mut Workspace, cx| { workspace.toggle_collab_panel(); }), ) @@ -174,27 +174,27 @@ impl StatusBar { .child( IconButton::::new("terminal", Icon::Terminal) .when(workspace.is_terminal_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) - .on_click(|workspace, cx| { + .on_click(|workspace: &mut Workspace, cx| { workspace.toggle_terminal(cx); }), ) .child( IconButton::::new("chat_panel", Icon::MessageBubbles) .when(workspace.is_chat_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) - .on_click(|workspace, cx| { + .on_click(|workspace: &mut Workspace, cx| { workspace.toggle_chat_panel(cx); }), ) .child( IconButton::::new("assistant_panel", Icon::Ai) .when(workspace.is_assistant_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) - .on_click(|workspace, cx| { + .on_click(|workspace: &mut Workspace, cx| { workspace.toggle_assistant_panel(cx); }), ), diff --git a/crates/ui2/src/to_extract/tab_bar.rs b/crates/ui2/src/to_extract/tab_bar.rs index aff095c639ea48c560b35e738e18f60ea38e8264..3b4b5cc2205ebd015ba40b050d058384897be1e4 100644 --- a/crates/ui2/src/to_extract/tab_bar.rs +++ b/crates/ui2/src/to_extract/tab_bar.rs @@ -1,5 +1,5 @@ -use crate::prelude::*; -use crate::{Icon, IconButton, Tab}; +use crate::{prelude::*, Icon, IconButton, Tab}; +use gpui::prelude::*; #[derive(Component)] pub struct TabBar { diff --git a/crates/ui2/src/to_extract/title_bar.rs b/crates/ui2/src/to_extract/title_bar.rs index 87d7dd4146d051aa5c8e4b8ae6697f487bfb34fa..1a106cbf7a5708143aed61b4c6aa5f32c955415a 100644 --- a/crates/ui2/src/to_extract/title_bar.rs +++ b/crates/ui2/src/to_extract/title_bar.rs @@ -6,8 +6,8 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::settings::user_settings; use crate::{ - Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus, - ScreenShareStatus, ToolDivider, TrafficLights, + Avatar, Button, Icon, IconButton, MicStatus, PlayerStack, PlayerWithCallStatus, + ScreenShareStatus, TextColor, ToolDivider, TrafficLights, }; #[derive(Clone)] @@ -152,21 +152,25 @@ impl Render for TitleBar { .gap_1() .child( IconButton::::new("toggle_mic_status", Icon::Mic) - .when(self.is_mic_muted(), |this| this.color(IconColor::Error)) - .on_click(|title_bar, cx| title_bar.toggle_mic_status(cx)), + .when(self.is_mic_muted(), |this| this.color(TextColor::Error)) + .on_click(|title_bar: &mut TitleBar, cx| { + title_bar.toggle_mic_status(cx) + }), ) .child( IconButton::::new("toggle_deafened", Icon::AudioOn) - .when(self.is_deafened, |this| this.color(IconColor::Error)) - .on_click(|title_bar, cx| title_bar.toggle_deafened(cx)), + .when(self.is_deafened, |this| this.color(TextColor::Error)) + .on_click(|title_bar: &mut TitleBar, cx| { + title_bar.toggle_deafened(cx) + }), ) .child( IconButton::::new("toggle_screen_share", Icon::Screen) .when( self.screen_share_status == ScreenShareStatus::Shared, - |this| this.color(IconColor::Accent), + |this| this.color(TextColor::Accent), ) - .on_click(|title_bar, cx| { + .on_click(|title_bar: &mut TitleBar, cx| { title_bar.toggle_screen_share_status(cx) }), ), diff --git a/crates/ui2/src/to_extract/workspace.rs b/crates/ui2/src/to_extract/workspace.rs index d6de8a828807606a6d55086af724983ca1a93df0..0451a9d032666f827335d843c2c06124650300d1 100644 --- a/crates/ui2/src/to_extract/workspace.rs +++ b/crates/ui2/src/to_extract/workspace.rs @@ -206,13 +206,14 @@ impl Render for Workspace { .child(self.editor_1.clone())], SplitDirection::Horizontal, ); + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); div() .relative() .size_full() .flex() .flex_col() - .font("Zed Sans") + .font(ui_font) .gap_0() .justify_start() .items_start() diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index f21eb84ae21eb59873f63110f2ffeb9696614d13..455148391bb4c470e822711266a169fb4f2403d0 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,8 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentElement, Render, - Subscription, View, ViewContext, WeakView, WindowContext, + div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter, + FocusHandle, ParentComponent, Render, Styled, Subscription, View, ViewContext, WeakView, + WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -34,6 +35,7 @@ pub trait Panel: Render + EventEmitter { fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} fn has_focus(&self, cx: &WindowContext) -> bool; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; } pub trait PanelHandle: Send + Sync { @@ -51,6 +53,7 @@ pub trait PanelHandle: Send + Sync { fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); fn icon_label(&self, cx: &WindowContext) -> Option; fn has_focus(&self, cx: &WindowContext) -> bool; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn to_any(&self) -> AnyView; } @@ -117,6 +120,10 @@ where fn to_any(&self) -> AnyView { self.clone().into() } + + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + self.read(cx).focus_handle(cx).clone() + } } impl From<&dyn PanelHandle> for AnyView { @@ -422,7 +429,18 @@ impl Render for Dock { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() + if let Some(entry) = self.visible_entry() { + let size = entry.panel.size(cx); + + div() + .map(|this| match self.position().axis() { + Axis::Horizontal => this.w(px(size)).h_full(), + Axis::Vertical => this.h(px(size)).w_full(), + }) + .child(entry.panel.to_any()) + } else { + div() + } } } @@ -728,5 +746,9 @@ pub mod test { fn has_focus(&self, _cx: &WindowContext) -> bool { self.has_focus } + + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + unimplemented!() + } } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index b3a5de8fb26d9caf25d652347b9a24ce47818630..c91df732c75b442c43baf2e05df439c4d0474ac6 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,6 +1,6 @@ use gpui::{ - div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive, - Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View, + ViewContext, WindowContext, }; use ui::{h_stack, v_stack}; @@ -71,6 +71,14 @@ impl ModalLayer { cx.notify(); } + + pub fn current_modal(&self) -> Option> + where + V: 'static, + { + let active_modal = self.active_modal.as_ref()?; + active_modal.modal.clone().downcast::().ok() + } } impl Render for ModalLayer { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index e3ea4863c999e1d0ce730c91f9091c954713c359..67ecc16165fd2bb7d256115c7607a86bd15ac180 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1,5 +1,3 @@ -// mod dragged_item_receiver; - use crate::{ item::{Item, ItemHandle, ItemSettings, WeakItemHandle}, toolbar::Toolbar, @@ -9,7 +7,7 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - actions, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, + actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -27,7 +25,7 @@ use std::{ }, }; use ui::v_stack; -use ui::{prelude::*, Icon, IconButton, IconColor, IconElement, TextTooltip}; +use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip}; use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] @@ -1432,13 +1430,13 @@ impl Pane { Some( IconElement::new(Icon::ExclamationTriangle) .size(ui::IconSize::Small) - .color(IconColor::Warning), + .color(TextColor::Warning), ) } else if item.is_dirty(cx) { Some( IconElement::new(Icon::ExclamationTriangle) .size(ui::IconSize::Small) - .color(IconColor::Info), + .color(TextColor::Info), ) } else { None @@ -1919,7 +1917,7 @@ impl Render for Pane { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() - .context("Pane") + .key_context("Pane") .size_full() .on_action(|pane: &mut Self, action, cx| { pane.close_active_item(action, cx) diff --git a/crates/workspace2/src/pane/dragged_item_receiver.rs b/crates/workspace2/src/pane/dragged_item_receiver.rs index d8e967dd7564236abda81ea4d3311468c7bb2f79..3e1f6393a608b31317765b81a7b71ace81aacce7 100644 --- a/crates/workspace2/src/pane/dragged_item_receiver.rs +++ b/crates/workspace2/src/pane/dragged_item_receiver.rs @@ -2,7 +2,7 @@ use super::DraggedItem; use crate::{Pane, SplitDirection, Workspace}; use gpui::{ color::Color, - elements::{Canvas, MouseEventHandler, ParentElement, Stack}, + elements::{Canvas, MouseEventHandler, ParentComponent, Stack}, geometry::{rect::RectF, vector::Vector2F}, platform::MouseButton, scene::MouseUp, diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index fcf6ac3b613219aaf8fa7556de101fb0df5554a5..5dccac243f47c0ec169c3bec9436d7c0a998b22f 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -2,7 +2,7 @@ use std::any::TypeId; use crate::{ItemHandle, Pane}; use gpui::{ - div, AnyView, Component, Div, ParentElement, Render, Styled, Subscription, View, ViewContext, + div, AnyView, Component, Div, ParentComponent, Render, Styled, Subscription, View, ViewContext, WindowContext, }; use theme2::ActiveTheme; diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 575ab6b8bd3363901b1f321f068421291c985ae8..13997e75880ac8c2ede2c8ab891ea25aec21adb0 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -29,23 +29,22 @@ use client2::{ Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; -use dock::{Dock, DockPosition, PanelButtons}; +use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _}; use futures::{ channel::{mpsc, oneshot}, future::try_join_all, Future, FutureExt, StreamExt, }; use gpui::{ - actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, - AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, - View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, - WindowOptions, + actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView, + AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, + EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent, + Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds, + WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; -use language2::LanguageRegistry; +use language2::{LanguageRegistry, Rope}; use lazy_static::lazy_static; pub use modal_layer::*; use node_runtime::NodeRuntime; @@ -67,12 +66,13 @@ use std::{ sync::{atomic::AtomicUsize, Arc}, time::Duration, }; -use theme2::ActiveTheme; +use theme2::{ActiveTheme, ThemeSettings}; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Button, ButtonVariant, Label, LabelColor}; +use ui::TextColor; +use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip}; use util::ResultExt; use uuid::Uuid; -use workspace_settings::{AutosaveSetting, WorkspaceSettings}; +pub use workspace_settings::{AutosaveSetting, WorkspaceSettings}; lazy_static! { static ref ZED_WINDOW_SIZE: Option> = env::var("ZED_WINDOW_SIZE") @@ -248,102 +248,6 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { // } // } // }); - // cx.add_async_action(Workspace::open); - - // cx.add_async_action(Workspace::follow_next_collaborator); - // cx.add_async_action(Workspace::close); - // cx.add_async_action(Workspace::close_inactive_items_and_panes); - // cx.add_async_action(Workspace::close_all_items_and_panes); - // cx.add_global_action(Workspace::close_global); - // cx.add_global_action(restart); - // cx.add_async_action(Workspace::save_all); - // cx.add_action(Workspace::add_folder_to_project); - // cx.add_action( - // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { - // let pane = workspace.active_pane().clone(); - // workspace.unfollow(&pane, cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { - // workspace - // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { - // workspace - // .save_active_item(SaveIntent::SaveAs, cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { - // workspace.activate_previous_pane(cx) - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { - // workspace.activate_next_pane(cx) - // }); - - // cx.add_action( - // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { - // workspace.activate_pane_in_direction(action.0, cx) - // }, - // ); - - // cx.add_action( - // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { - // workspace.swap_pane_in_direction(action.0, cx) - // }, - // ); - - // cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { - // workspace.toggle_dock(DockPosition::Left, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { - // workspace.toggle_dock(DockPosition::Bottom, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { - // workspace.close_all_docks(cx); - // }); - // cx.add_action(Workspace::activate_pane_at_index); - // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { - // workspace.reopen_closed_item(cx).detach(); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { - // workspace - // .go_back(workspace.active_pane().downgrade(), cx) - // .detach(); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { - // workspace - // .go_forward(workspace.active_pane().downgrade(), cx) - // .detach(); - // }); - - // cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { - // cx.spawn(|workspace, mut cx| async move { - // let err = install_cli::install_cli(&cx) - // .await - // .context("Failed to create CLI symlink"); - - // workspace.update(&mut cx, |workspace, cx| { - // if matches!(err, Err(_)) { - // err.notify_err(workspace, cx); - // } else { - // workspace.show_notification(1, cx, |cx| { - // cx.build_view(|_| { - // MessageNotification::new("Successfully installed the `zed` binary") - // }) - // }); - // } - // }) - // }) - // .detach(); - // }); } type ProjectItemBuilders = @@ -443,7 +347,6 @@ struct Follower { impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { - use gpui::Context; use node_runtime::FakeNodeRuntime; use settings2::SettingsStore; @@ -531,13 +434,7 @@ pub enum Event { pub struct Workspace { weak_self: WeakView, focus_handle: FocusHandle, - workspace_actions: Vec< - Box< - dyn Fn( - Div>, - ) -> Div>, - >, - >, + workspace_actions: Vec) -> Div>>, zoomed: Option, zoomed_position: Option, center: PaneGroup, @@ -942,108 +839,15 @@ impl Workspace { &self.right_dock } - // pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) - // where - // T::Event: std::fmt::Debug, - // { - // self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {}) - // } - - // pub fn add_panel_with_extra_event_handler( - // &mut self, - // panel: View, - // cx: &mut ViewContext, - // handler: F, - // ) where - // T::Event: std::fmt::Debug, - // F: Fn(&mut Self, &View, &T::Event, &mut ViewContext) + 'static, - // { - // let dock = match panel.position(cx) { - // DockPosition::Left => &self.left_dock, - // DockPosition::Bottom => &self.bottom_dock, - // DockPosition::Right => &self.right_dock, - // }; - - // self.subscriptions.push(cx.subscribe(&panel, { - // let mut dock = dock.clone(); - // let mut prev_position = panel.position(cx); - // move |this, panel, event, cx| { - // if T::should_change_position_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // let new_position = panel.read(cx).position(cx); - // let mut was_visible = false; - // dock.update(cx, |dock, cx| { - // prev_position = new_position; - - // was_visible = dock.is_open() - // && dock - // .visible_panel() - // .map_or(false, |active_panel| active_panel.id() == panel.id()); - // dock.remove_panel(&panel, cx); - // }); - - // if panel.is_zoomed(cx) { - // this.zoomed_position = Some(new_position); - // } - - // dock = match panel.read(cx).position(cx) { - // DockPosition::Left => &this.left_dock, - // DockPosition::Bottom => &this.bottom_dock, - // DockPosition::Right => &this.right_dock, - // } - // .clone(); - // dock.update(cx, |dock, cx| { - // dock.add_panel(panel.clone(), cx); - // if was_visible { - // dock.set_open(true, cx); - // dock.activate_panel(dock.panels_len() - 1, cx); - // } - // }); - // } else if T::should_zoom_in_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); - // if !panel.has_focus(cx) { - // cx.focus(&panel); - // } - // this.zoomed = Some(panel.downgrade().into_any()); - // this.zoomed_position = Some(panel.read(cx).position(cx)); - // } else if T::should_zoom_out_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); - // if this.zoomed_position == Some(prev_position) { - // this.zoomed = None; - // this.zoomed_position = None; - // } - // cx.notify(); - // } else if T::is_focus_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // let position = panel.read(cx).position(cx); - // this.dismiss_zoomed_items_to_reveal(Some(position), cx); - // if panel.is_zoomed(cx) { - // this.zoomed = Some(panel.downgrade().into_any()); - // this.zoomed_position = Some(position); - // } else { - // this.zoomed = None; - // this.zoomed_position = None; - // } - // this.update_active_view_for_followers(cx); - // cx.notify(); - // } else { - // handler(this, &panel, event, cx) - // } - // } - // })); + pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { + let dock = match panel.position(cx) { + DockPosition::Left => &self.left_dock, + DockPosition::Bottom => &self.bottom_dock, + DockPosition::Right => &self.right_dock, + }; - // dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); - // } + dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); + } pub fn status_bar(&self) -> &View { &self.status_bar @@ -1241,29 +1045,29 @@ impl Workspace { // self.titlebar_item.clone() // } - // /// Call the given callback with a workspace whose project is local. - // /// - // /// If the given workspace has a local project, then it will be passed - // /// to the callback. Otherwise, a new empty window will be created. - // pub fn with_local_workspace( - // &mut self, - // cx: &mut ViewContext, - // callback: F, - // ) -> Task> - // where - // T: 'static, - // F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> T, - // { - // if self.project.read(cx).is_local() { - // Task::Ready(Some(Ok(callback(self, cx)))) - // } else { - // let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx); - // cx.spawn(|_vh, mut cx| async move { - // let (workspace, _) = task.await; - // workspace.update(&mut cx, callback) - // }) - // } - // } + /// Call the given callback with a workspace whose project is local. + /// + /// If the given workspace has a local project, then it will be passed + /// to the callback. Otherwise, a new empty window will be created. + pub fn with_local_workspace( + &mut self, + cx: &mut ViewContext, + callback: F, + ) -> Task> + where + T: 'static, + F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> T, + { + if self.project.read(cx).is_local() { + Task::Ready(Some(Ok(callback(self, cx)))) + } else { + let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx); + cx.spawn(|_vh, mut cx| async move { + let (workspace, _) = task.await?; + workspace.update(&mut cx, callback) + }) + } + } pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator> { self.project.read(cx).worktrees() @@ -1735,42 +1539,43 @@ impl Workspace { // } // } - // pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { - // let dock = match dock_side { - // DockPosition::Left => &self.left_dock, - // DockPosition::Bottom => &self.bottom_dock, - // DockPosition::Right => &self.right_dock, - // }; - // let mut focus_center = false; - // let mut reveal_dock = false; - // dock.update(cx, |dock, cx| { - // let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); - // let was_visible = dock.is_open() && !other_is_zoomed; - // dock.set_open(!was_visible, cx); - - // if let Some(active_panel) = dock.active_panel() { - // if was_visible { - // if active_panel.has_focus(cx) { - // focus_center = true; - // } - // } else { - // cx.focus(active_panel.as_any()); - // reveal_dock = true; - // } - // } - // }); + pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { + let dock = match dock_side { + DockPosition::Left => &self.left_dock, + DockPosition::Bottom => &self.bottom_dock, + DockPosition::Right => &self.right_dock, + }; + let mut focus_center = false; + let mut reveal_dock = false; + dock.update(cx, |dock, cx| { + let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); + let was_visible = dock.is_open() && !other_is_zoomed; + dock.set_open(!was_visible, cx); + + if let Some(active_panel) = dock.active_panel() { + if was_visible { + if active_panel.has_focus(cx) { + focus_center = true; + } + } else { + let focus_handle = &active_panel.focus_handle(cx); + cx.focus(focus_handle); + reveal_dock = true; + } + } + }); - // if reveal_dock { - // self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); - // } + if reveal_dock { + self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); + } - // if focus_center { - // cx.focus_self(); - // } + if focus_center { + cx.focus(&self.focus_handle); + } - // cx.notify(); - // self.serialize_workspace(cx); - // } + cx.notify(); + self.serialize_workspace(cx); + } pub fn close_all_docks(&mut self, cx: &mut ViewContext) { let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock]; @@ -1961,50 +1766,50 @@ impl Workspace { }) } - // pub fn open_abs_path( - // &mut self, - // abs_path: PathBuf, - // visible: bool, - // cx: &mut ViewContext, - // ) -> Task>> { - // cx.spawn(|workspace, mut cx| async move { - // let open_paths_task_result = workspace - // .update(&mut cx, |workspace, cx| { - // workspace.open_paths(vec![abs_path.clone()], visible, cx) - // }) - // .with_context(|| format!("open abs path {abs_path:?} task spawn"))? - // .await; - // anyhow::ensure!( - // open_paths_task_result.len() == 1, - // "open abs path {abs_path:?} task returned incorrect number of results" - // ); - // match open_paths_task_result - // .into_iter() - // .next() - // .expect("ensured single task result") - // { - // Some(open_result) => { - // open_result.with_context(|| format!("open abs path {abs_path:?} task join")) - // } - // None => anyhow::bail!("open abs path {abs_path:?} task returned None"), - // } - // }) - // } + pub fn open_abs_path( + &mut self, + abs_path: PathBuf, + visible: bool, + cx: &mut ViewContext, + ) -> Task>> { + cx.spawn(|workspace, mut cx| async move { + let open_paths_task_result = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_paths(vec![abs_path.clone()], visible, cx) + }) + .with_context(|| format!("open abs path {abs_path:?} task spawn"))? + .await; + anyhow::ensure!( + open_paths_task_result.len() == 1, + "open abs path {abs_path:?} task returned incorrect number of results" + ); + match open_paths_task_result + .into_iter() + .next() + .expect("ensured single task result") + { + Some(open_result) => { + open_result.with_context(|| format!("open abs path {abs_path:?} task join")) + } + None => anyhow::bail!("open abs path {abs_path:?} task returned None"), + } + }) + } - // pub fn split_abs_path( - // &mut self, - // abs_path: PathBuf, - // visible: bool, - // cx: &mut ViewContext, - // ) -> Task>> { - // let project_path_task = - // Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); - // cx.spawn(|this, mut cx| async move { - // let (_, path) = project_path_task.await?; - // this.update(&mut cx, |this, cx| this.split_path(path, cx))? - // .await - // }) - // } + pub fn split_abs_path( + &mut self, + abs_path: PathBuf, + visible: bool, + cx: &mut ViewContext, + ) -> Task>> { + let project_path_task = + Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); + cx.spawn(|this, mut cx| async move { + let (_, path) = project_path_task.await?; + this.update(&mut cx, |this, cx| this.split_path(path, cx))? + .await + }) + } pub fn open_path( &mut self, @@ -2031,37 +1836,37 @@ impl Workspace { }) } - // pub fn split_path( - // &mut self, - // path: impl Into, - // cx: &mut ViewContext, - // ) -> Task, anyhow::Error>> { - // let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { - // self.panes - // .first() - // .expect("There must be an active pane") - // .downgrade() - // }); + pub fn split_path( + &mut self, + path: impl Into, + cx: &mut ViewContext, + ) -> Task, anyhow::Error>> { + let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { + self.panes + .first() + .expect("There must be an active pane") + .downgrade() + }); - // if let Member::Pane(center_pane) = &self.center.root { - // if center_pane.read(cx).items_len() == 0 { - // return self.open_path(path, Some(pane), true, cx); - // } - // } + if let Member::Pane(center_pane) = &self.center.root { + if center_pane.read(cx).items_len() == 0 { + return self.open_path(path, Some(pane), true, cx); + } + } - // let task = self.load_path(path.into(), cx); - // cx.spawn(|this, mut cx| async move { - // let (project_entry_id, build_item) = task.await?; - // this.update(&mut cx, move |this, cx| -> Option<_> { - // let pane = pane.upgrade(cx)?; - // let new_pane = this.split_pane(pane, SplitDirection::Right, cx); - // new_pane.update(cx, |new_pane, cx| { - // Some(new_pane.open_item(project_entry_id, true, cx, build_item)) - // }) - // }) - // .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))? - // }) - // } + let task = self.load_path(path.into(), cx); + cx.spawn(|this, mut cx| async move { + let (project_entry_id, build_item) = task.await?; + this.update(&mut cx, move |this, cx| -> Option<_> { + let pane = pane.upgrade()?; + let new_pane = this.split_pane(pane, SplitDirection::Right, cx); + new_pane.update(cx, |new_pane, cx| { + Some(new_pane.open_item(project_entry_id, true, cx, build_item)) + }) + }) + .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))? + }) + } pub(crate) fn load_path( &mut self, @@ -2660,17 +2465,50 @@ impl Workspace { h_stack() // TODO - Add player menu .child( - Button::new("player") - .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Player(0))), + div() + .id("project_owner_indicator") + .child( + Button::new("player") + .variant(ButtonVariant::Ghost) + .color(Some(TextColor::Player(0))), + ) + .tooltip(move |_, cx| { + cx.build_view(|cx| TextTooltip::new("Toggle following")) + }), ) // TODO - Add project menu - .child(Button::new("project_name").variant(ButtonVariant::Ghost)) + .child( + div() + .id("titlebar_project_menu_button") + .child(Button::new("project_name").variant(ButtonVariant::Ghost)) + .tooltip(move |_, cx| { + cx.build_view(|cx| TextTooltip::new("Recent Projects")) + }), + ) // TODO - Add git menu .child( - Button::new("branch_name") - .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Muted)), + div() + .id("titlebar_git_menu_button") + .child( + Button::new("branch_name") + .variant(ButtonVariant::Ghost) + .color(Some(TextColor::Muted)), + ) + .tooltip(move |_, cx| { + // todo!() Replace with real action. + #[gpui::action] + struct NoAction {} + + cx.build_view(|cx| { + TextTooltip::new("Recent Branches") + .key_binding(KeyBinding::new(gpui::KeyBinding::new( + "cmd-b", + NoAction {}, + None, + ))) + .meta("Only local branches shown") + }) + }), ), ) // self.titlebar_item .child(h_stack().child(Label::new("Right side titlebar item"))) @@ -3192,10 +3030,10 @@ impl Workspace { fn force_remove_pane(&mut self, pane: &View, cx: &mut ViewContext) { self.panes.retain(|p| p != pane); - if true { - todo!() - // cx.focus(self.panes.last().unwrap()); - } + self.panes + .last() + .unwrap() + .update(cx, |pane, cx| pane.focus(cx)); if self.last_active_center_pane == Some(pane.downgrade()) { self.last_active_center_pane = None; } @@ -3467,9 +3305,105 @@ impl Workspace { }) } + fn actions(div: Div) -> Div { + div + // cx.add_async_action(Workspace::open); + // cx.add_async_action(Workspace::follow_next_collaborator); + // cx.add_async_action(Workspace::close); + // cx.add_async_action(Workspace::close_inactive_items_and_panes); + // cx.add_async_action(Workspace::close_all_items_and_panes); + // cx.add_global_action(Workspace::close_global); + // cx.add_global_action(restart); + // cx.add_async_action(Workspace::save_all); + // cx.add_action(Workspace::add_folder_to_project); + // cx.add_action( + // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { + // let pane = workspace.active_pane().clone(); + // workspace.unfollow(&pane, cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { + // workspace + // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { + // workspace + // .save_active_item(SaveIntent::SaveAs, cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { + // workspace.activate_previous_pane(cx) + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { + // workspace.activate_next_pane(cx) + // }); + // cx.add_action( + // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { + // workspace.activate_pane_in_direction(action.0, cx) + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { + // workspace.swap_pane_in_direction(action.0, cx) + // }, + // ); + .on_action(|this, e: &ToggleLeftDock, cx| { + println!("TOGGLING DOCK"); + this.toggle_dock(DockPosition::Left, cx); + }) + // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + // workspace.toggle_dock(DockPosition::Right, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + // workspace.toggle_dock(DockPosition::Bottom, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + // workspace.close_all_docks(cx); + // }); + // cx.add_action(Workspace::activate_pane_at_index); + // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { + // workspace.reopen_closed_item(cx).detach(); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { + // workspace + // .go_back(workspace.active_pane().downgrade(), cx) + // .detach(); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { + // workspace + // .go_forward(workspace.active_pane().downgrade(), cx) + // .detach(); + // }); + + // cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { + // cx.spawn(|workspace, mut cx| async move { + // let err = install_cli::install_cli(&cx) + // .await + // .context("Failed to create CLI symlink"); + + // workspace.update(&mut cx, |workspace, cx| { + // if matches!(err, Err(_)) { + // err.notify_err(workspace, cx); + // } else { + // workspace.show_notification(1, cx, |cx| { + // cx.build_view(|_| { + // MessageNotification::new("Successfully installed the `zed` binary") + // }) + // }); + // } + // }) + // }) + // .detach(); + // }); + } + #[cfg(any(test, feature = "test-support"))] pub fn test_new(project: Model, cx: &mut ViewContext) -> Self { - use gpui::Context; use node_runtime::FakeNodeRuntime; let client = project.read(cx).client(); @@ -3486,7 +3420,9 @@ impl Workspace { initialize_workspace: |_, _, _, _| Task::ready(Ok(())), node_runtime: FakeNodeRuntime::new(), }); - Self::new(0, project, app_state, cx) + let workspace = Self::new(0, project, app_state, cx); + workspace.active_pane.update(cx, |pane, cx| pane.focus(cx)); + workspace } // fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { @@ -3522,25 +3458,27 @@ impl Workspace { pub fn register_action( &mut self, callback: impl Fn(&mut Self, &A, &mut ViewContext) + 'static, - ) { + ) -> &mut Self { let callback = Arc::new(callback); self.workspace_actions.push(Box::new(move |div| { let callback = callback.clone(); div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx)) })); + self } - fn add_workspace_actions_listeners( - &self, - mut div: Div>, - ) -> Div> { + fn add_workspace_actions_listeners(&self, mut div: Div) -> Div { for action in self.workspace_actions.iter() { div = (action)(div) } div } + pub fn current_modal(&mut self, cx: &ViewContext) -> Option> { + self.modal_layer.read(cx).current_modal() + } + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) where B: FnOnce(&mut ViewContext) -> V, @@ -3764,14 +3702,15 @@ impl Render for Workspace { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); self.add_workspace_actions_listeners(div()) - .context(context) + .key_context(context) .relative() .size_full() .flex() .flex_col() - .font("Zed Sans") + .font(ui_font) .gap_0() .justify_start() .items_start() @@ -3791,83 +3730,46 @@ impl Render for Workspace { .border_b() .border_color(cx.theme().colors().border) .child(self.modal_layer.clone()) - // .children( - // Some( - // Panel::new("project-panel-outer", cx) - // .side(PanelSide::Left) - // .child(ProjectPanel::new("project-panel-inner")), - // ) - // .filter(|_| self.is_project_panel_open()), - // ) - // .children( - // Some( - // Panel::new("collab-panel-outer", cx) - // .child(CollabPanel::new("collab-panel-inner")) - // .side(PanelSide::Left), - // ) - // .filter(|_| self.is_collab_panel_open()), - // ) - // .child(NotificationToast::new( - // "maxbrunsfeld has requested to add you as a contact.".into(), - // )) .child( - div().flex().flex_col().flex_1().h_full().child( - div().flex().flex_1().child(self.center.render( - &self.project, - &self.follower_states, - self.active_call(), - &self.active_pane, - self.zoomed.as_ref(), - &self.app_state, - cx, - )), - ), // .children( - // Some( - // Panel::new("terminal-panel", cx) - // .child(Terminal::new()) - // .allowed_sides(PanelAllowedSides::BottomOnly) - // .side(PanelSide::Bottom), - // ) - // .filter(|_| self.is_terminal_open()), - // ), - ), // .children( - // Some( - // Panel::new("chat-panel-outer", cx) - // .side(PanelSide::Right) - // .child(ChatPanel::new("chat-panel-inner").messages(vec![ - // ChatMessage::new( - // "osiewicz".to_string(), - // "is this thing on?".to_string(), - // DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z") - // .unwrap() - // .naive_local(), - // ), - // ChatMessage::new( - // "maxdeviant".to_string(), - // "Reading you loud and clear!".to_string(), - // DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z") - // .unwrap() - // .naive_local(), - // ), - // ])), - // ) - // .filter(|_| self.is_chat_panel_open()), - // ) - // .children( - // Some( - // Panel::new("notifications-panel-outer", cx) - // .side(PanelSide::Right) - // .child(NotificationsPanel::new("notifications-panel-inner")), - // ) - // .filter(|_| self.is_notifications_panel_open()), - // ) - // .children( - // Some( - // Panel::new("assistant-panel-outer", cx) - // .child(AssistantPanel::new("assistant-panel-inner")), - // ) - // .filter(|_| self.is_assistant_panel_open()), - // ), + div() + .flex() + .flex_row() + .flex_1() + .h_full() + // Left Dock + .child( + div() + .flex() + .flex_none() + .overflow_hidden() + .child(self.left_dock.clone()), + ) + // Panes + .child( + div() + .flex() + .flex_col() + .flex_1() + .child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )) + .child(div().flex().flex_1().child(self.bottom_dock.clone())), + ) + // Right Dock + .child( + div() + .flex() + .flex_none() + .overflow_hidden() + .child(self.right_dock.clone()), + ), + ), ) .child(self.status_bar.clone()) // .when(self.debug.show_toast, |this| { @@ -4522,32 +4424,32 @@ pub fn open_new( }) } -// pub fn create_and_open_local_file( -// path: &'static Path, -// cx: &mut ViewContext, -// default_content: impl 'static + Send + FnOnce() -> Rope, -// ) -> Task>> { -// cx.spawn(|workspace, mut cx| async move { -// let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; -// if !fs.is_file(path).await { -// fs.create_file(path, Default::default()).await?; -// fs.save(path, &default_content(), Default::default()) -// .await?; -// } +pub fn create_and_open_local_file( + path: &'static Path, + cx: &mut ViewContext, + default_content: impl 'static + Send + FnOnce() -> Rope, +) -> Task>> { + cx.spawn(|workspace, mut cx| async move { + let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; + if !fs.is_file(path).await { + fs.create_file(path, Default::default()).await?; + fs.save(path, &default_content(), Default::default()) + .await?; + } -// let mut items = workspace -// .update(&mut cx, |workspace, cx| { -// workspace.with_local_workspace(cx, |workspace, cx| { -// workspace.open_paths(vec![path.to_path_buf()], false, cx) -// }) -// })? -// .await? -// .await; + let mut items = workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(cx, |workspace, cx| { + workspace.open_paths(vec![path.to_path_buf()], false, cx) + }) + })? + .await? + .await; -// let item = items.pop().flatten(); -// item.ok_or_else(|| anyhow!("path {path:?} is not a file"))? -// }) -// } + let item = items.pop().flatten(); + item.ok_or_else(|| anyhow!("path {path:?} is not a file"))? + }) +} // pub fn join_remote_project( // project_id: u64, diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 570912abc579a7aad2461c4fe081354c97fcc478..dedb12c08c479a08b20953165e5fc831b49795a2 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -36,14 +36,14 @@ copilot = { package = "copilot2", path = "../copilot2" } db = { package = "db2", path = "../db2" } editor = { package="editor2", path = "../editor2" } # feedback = { path = "../feedback" } -# file_finder = { path = "../file_finder" } +file_finder = { package="file_finder2", path = "../file_finder2" } # search = { path = "../search" } fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { package = "go_to_line2", path = "../go_to_line2" } gpui = { package = "gpui2", path = "../gpui2" } -install_cli = { path = "../install_cli" } +install_cli = { package = "install_cli2", path = "../install_cli2" } journal = { package = "journal2", path = "../journal2" } language = { package = "language2", path = "../language2" } # language_selector = { path = "../language_selector" } @@ -55,7 +55,7 @@ node_runtime = { path = "../node_runtime" } # outline = { path = "../outline" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } -# project_panel = { path = "../project_panel" } +project_panel = { package = "project_panel2", path = "../project_panel2" } # project_symbols = { path = "../project_symbols" } # quick_action_bar = { path = "../quick_action_bar" } # recent_projects = { path = "../recent_projects" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 2deaff21491a73bd54bcf5d80a0d2ca9c7a76a7f..2a3d4d1195f5dceced4ced56a162fd75b8b1d1f9 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -50,14 +50,16 @@ use util::{ use uuid::Uuid; use workspace::{AppState, WorkspaceStore}; use zed2::{ - build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, - languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, + build_window_options, ensure_only_instance, handle_cli_connection, init_zed_actions, + initialize_workspace, languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, }; mod open_listener; fn main() { menu::init(); + zed_actions::init(); + let http = http::client(); init_paths(); init_logger(); @@ -96,7 +98,7 @@ fn main() { let (listener, mut open_rx) = OpenListener::new(); let listener = Arc::new(listener); let open_listener = listener.clone(); - app.on_open_urls(move |urls, _| open_listener.open_urls(urls)); + app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); app.on_reopen(move |_cx| { // todo!("workspace") // if cx.has_global::>() { @@ -111,6 +113,8 @@ fn main() { app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); + cx.set_global(listener.clone()); + load_embedded_fonts(cx); let mut store = SettingsStore::default(); @@ -186,10 +190,10 @@ fn main() { // recent_projects::init(cx); go_to_line::init(cx); - // file_finder::init(cx); + file_finder::init(cx); // outline::init(cx); // project_symbols::init(cx); - // project_panel::init(Assets, cx); + project_panel::init(Assets, cx); // channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); // search::init(cx); @@ -209,12 +213,13 @@ fn main() { // zed::init(&app_state, cx); // cx.set_menus(menus::menus()); + init_zed_actions(app_state.clone(), cx); if stdout_is_a_pty() { cx.activate(true); let urls = collect_url_args(); if !urls.is_empty() { - listener.open_urls(urls) + listener.open_urls(&urls) } } else { upload_previous_panics(http.clone(), cx); @@ -224,7 +229,7 @@ fn main() { if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() && !listener.triggered.load(Ordering::Acquire) { - listener.open_urls(collect_url_args()) + listener.open_urls(&collect_url_args()) } } diff --git a/crates/zed2/src/open_listener.rs b/crates/zed2/src/open_listener.rs index f4219f199dca3bfb4bbc01ea27a07cd5aa84c13d..4c961a2b317b6d0a2e5399fa0be3e7b1b219311f 100644 --- a/crates/zed2/src/open_listener.rs +++ b/crates/zed2/src/open_listener.rs @@ -54,7 +54,7 @@ impl OpenListener { ) } - pub fn open_urls(&self, urls: Vec) { + pub fn open_urls(&self, urls: &[String]) { self.triggered.store(true, Ordering::Release); let request = if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) @@ -101,7 +101,7 @@ impl OpenListener { None } - fn handle_file_urls(&self, urls: Vec) -> Option { + fn handle_file_urls(&self, urls: &[String]) -> Option { let paths: Vec<_> = urls .iter() .flat_map(|url| url.strip_prefix("file://")) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index de985496c8eeb30a6359aedac3122adf2d6dc816..2f7a38b041ac8f26145c0257676f8816ef5e8c18 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,5 +1,5 @@ -#![allow(unused_variables, dead_code, unused_mut)] -// todo!() this is to make transition easier. +#![allow(unused_variables, unused_mut)] +//todo!() mod assets; pub mod languages; @@ -7,17 +7,56 @@ mod only_instance; mod open_listener; pub use assets::*; +use collections::VecDeque; +use editor::{Editor, MultiBuffer}; use gpui::{ - point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, WeakView, WindowBounds, - WindowKind, WindowOptions, + actions, point, px, AppContext, AsyncWindowContext, Context, PromptLevel, Task, + TitlebarOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowKind, WindowOptions, }; pub use only_instance::*; pub use open_listener::*; -use anyhow::Result; -use std::sync::Arc; +use anyhow::{anyhow, Context as _, Result}; +use project_panel::ProjectPanel; +use settings::{initial_local_settings_content, Settings}; +use std::{borrow::Cow, ops::Deref, sync::Arc}; +use util::{ + asset_str, + channel::ReleaseChannel, + paths::{self, LOCAL_SETTINGS_RELATIVE_PATH}, + ResultExt, +}; use uuid::Uuid; -use workspace::{AppState, Workspace}; +use workspace::{ + create_and_open_local_file, dock::PanelHandle, + notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, + NewWindow, Workspace, WorkspaceSettings, +}; +use zed_actions::{OpenBrowser, OpenZedURL}; + +actions!( + About, + DebugElements, + DecreaseBufferFontSize, + Hide, + HideOthers, + IncreaseBufferFontSize, + Minimize, + OpenDefaultKeymap, + OpenDefaultSettings, + OpenKeymap, + OpenLicenses, + OpenLocalSettings, + OpenLog, + OpenSettings, + OpenTelemetryLog, + Quit, + ResetBufferFontSize, + ResetDatabase, + ShowAll, + ToggleFullScreen, + Zoom, +); pub fn build_window_options( bounds: Option, @@ -47,6 +86,211 @@ pub fn build_window_options( } } +pub fn init_zed_actions(app_state: Arc, cx: &mut AppContext) { + cx.observe_new_views(move |workspace: &mut Workspace, _cx| { + workspace + .register_action(about) + .register_action(|_, _: &Hide, cx| { + cx.hide(); + }) + .register_action(|_, _: &HideOthers, cx| { + cx.hide_other_apps(); + }) + .register_action(|_, _: &ShowAll, cx| { + cx.unhide_other_apps(); + }) + .register_action(|_, _: &Minimize, cx| { + cx.minimize_window(); + }) + .register_action(|_, _: &Zoom, cx| { + cx.zoom_window(); + }) + .register_action(|_, _: &ToggleFullScreen, cx| { + cx.toggle_full_screen(); + }) + .register_action(quit) + .register_action(|_, action: &OpenZedURL, cx| { + cx.global::>() + .open_urls(&[action.url.clone()]) + }) + .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url)) + //todo!(buffer font size) + // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size += 1.0) + // }); + // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size -= 1.0) + // }); + // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); + .register_action(|_, _: &install_cli::Install, cx| { + cx.spawn(|_, cx| async move { + install_cli::install_cli(cx.deref()) + .await + .context("error creating CLI symlink") + }) + .detach_and_log_err(cx); + }) + .register_action(|workspace, _: &OpenLog, cx| { + open_log_file(workspace, cx); + }) + .register_action(|workspace, _: &OpenLicenses, cx| { + open_bundled_file( + workspace, + asset_str::("licenses.md"), + "Open Source License Attribution", + "Markdown", + cx, + ); + }) + .register_action( + move |workspace: &mut Workspace, + _: &OpenTelemetryLog, + cx: &mut ViewContext| { + open_telemetry_log_file(workspace, cx); + }, + ) + .register_action( + move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { + create_and_open_local_file(&paths::KEYMAP, cx, Default::default) + .detach_and_log_err(cx); + }, + ) + .register_action( + move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + create_and_open_local_file(&paths::SETTINGS, cx, || { + settings::initial_user_settings_content().as_ref().into() + }) + .detach_and_log_err(cx); + }, + ) + .register_action(open_local_settings_file) + .register_action( + move |workspace: &mut Workspace, + _: &OpenDefaultKeymap, + cx: &mut ViewContext| { + open_bundled_file( + workspace, + settings::default_keymap(), + "Default Key Bindings", + "JSON", + cx, + ); + }, + ) + .register_action( + move |workspace: &mut Workspace, + _: &OpenDefaultSettings, + cx: &mut ViewContext| { + open_bundled_file( + workspace, + settings::default_settings(), + "Default Settings", + "JSON", + cx, + ); + }, + ) + //todo!() + // cx.add_action({ + // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + // let app_state = workspace.app_state().clone(); + // let markdown = app_state.languages.language_for_name("JSON"); + // let window = cx.window(); + // cx.spawn(|workspace, mut cx| async move { + // let markdown = markdown.await.log_err(); + // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { + // anyhow!("could not debug elements for window {}", window.id()) + // })?) + // .unwrap(); + // workspace + // .update(&mut cx, |workspace, cx| { + // workspace.with_local_workspace(cx, move |workspace, cx| { + // let project = workspace.project().clone(); + // let buffer = project + // .update(cx, |project, cx| { + // project.create_buffer(&content, markdown, cx) + // }) + // .expect("creating buffers on a local workspace always succeeds"); + // let buffer = cx.add_model(|cx| { + // MultiBuffer::singleton(buffer, cx) + // .with_title("Debug Elements".into()) + // }); + // workspace.add_item( + // Box::new(cx.add_view(|cx| { + // Editor::for_multibuffer(buffer, Some(project.clone()), cx) + // })), + // cx, + // ); + // }) + // })? + // .await + // }) + // .detach_and_log_err(cx); + // } + // }); + // .register_action( + // |workspace: &mut Workspace, + // _: &project_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::collab_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::chat_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::notification_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &terminal_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + .register_action({ + let app_state = Arc::downgrade(&app_state); + move |_, _: &NewWindow, cx| { + if let Some(app_state) = app_state.upgrade() { + open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + } + }) + .register_action({ + let app_state = Arc::downgrade(&app_state); + move |_, _: &NewFile, cx| { + if let Some(app_state) = app_state.upgrade() { + open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + } + }); + //todo!() + // load_default_keymap(cx); + }) + .detach(); +} + pub fn initialize_workspace( workspace_handle: WeakView, was_deserialized: bool, @@ -138,49 +382,38 @@ pub fn initialize_workspace( // } // false // }); - // })?; - - // let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); - // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); - // let channels_panel = - // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); - // let chat_panel = - // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); - // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( - // workspace_handle.clone(), - // cx.clone(), - // ); - // let ( - // project_panel, + })?; + + let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); + // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); + // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + // let channels_panel = + // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); + // let chat_panel = + // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); + // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( + // workspace_handle.clone(), + // cx.clone(), + // ); + let ( + project_panel, // terminal_panel, // assistant_panel, // channels_panel, // chat_panel, // notification_panel, - // ) = futures::try_join!( - // project_panel, + ) = futures::try_join!( + project_panel, // terminal_panel, // assistant_panel, // channels_panel, // chat_panel, // notification_panel, - // )?; - // workspace_handle.update(&mut cx, |workspace, cx| { - // let project_panel_position = project_panel.position(cx); - // workspace.add_panel_with_extra_event_handler( - // project_panel, - // cx, - // |workspace, _, event, cx| match event { - // project_panel::Event::NewSearchInDirectory { dir_entry } => { - // search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx) - // } - // project_panel::Event::ActivatePanel => { - // workspace.focus_panel::(cx); - // } - // _ => {} - // }, - // ); + )?; + + workspace_handle.update(&mut cx, |workspace, cx| { + let project_panel_position = project_panel.position(cx); + workspace.add_panel(project_panel, cx); // workspace.add_panel(terminal_panel, cx); // workspace.add_panel(assistant_panel, cx); // workspace.add_panel(channels_panel, cx); @@ -198,10 +431,286 @@ pub fn initialize_workspace( // .map_or(false, |entry| entry.is_dir()) // }) // { - // workspace.toggle_dock(project_panel_position, cx); + workspace.toggle_dock(project_panel_position, cx); // } - // cx.focus_self(); + // cx.focus_self(); })?; Ok(()) }) } + +fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { + let app_name = cx.global::().display_name(); + let version = env!("CARGO_PKG_VERSION"); + let prompt = cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]); + cx.foreground_executor() + .spawn(async { + prompt.await.ok(); + }) + .detach(); +} + +fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { + let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; + cx.spawn(|_, mut cx| async move { + let mut workspace_windows = cx.update(|_, cx| { + cx.windows() + .into_iter() + .filter_map(|window| window.downcast::()) + .collect::>() + })?; + + // If multiple windows have unsaved changes, and need a save prompt, + // prompt in the active window before switching to a different window. + cx.update(|_, cx| { + workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + }) + .log_err(); + + if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { + let answer = cx + .update(|_, cx| { + cx.prompt( + PromptLevel::Info, + "Are you sure you want to quit?", + &["Quit", "Cancel"], + ) + }) + .log_err(); + + if let Some(mut answer) = answer { + let answer = answer.await.ok(); + if answer != Some(0) { + return Ok(()); + } + } + } + + // If the user cancels any save prompt, then keep the app open. + for window in workspace_windows { + if let Some(should_close) = window + .update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + }) + .log_err() + { + if !should_close.await? { + return Ok(()); + } + } + } + cx.update(|_, cx| { + cx.quit(); + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); +} + +fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { + const MAX_LINES: usize = 1000; + workspace + .with_local_workspace(cx, move |workspace, cx| { + let fs = workspace.app_state().fs.clone(); + cx.spawn(|workspace, mut cx| async move { + let (old_log, new_log) = + futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG)); + + let mut lines = VecDeque::with_capacity(MAX_LINES); + for line in old_log + .iter() + .flat_map(|log| log.lines()) + .chain(new_log.iter().flat_map(|log| log.lines())) + { + if lines.len() == MAX_LINES { + lines.pop_front(); + } + lines.push_back(line); + } + let log = lines + .into_iter() + .flat_map(|line| [line, "\n"]) + .collect::(); + + workspace + .update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx)); + + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Log".into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| { + Editor::for_multibuffer(buffer, Some(project), cx) + })), + cx, + ); + }) + .log_err(); + }) + .detach(); + }) + .detach(); +} + +fn open_local_settings_file( + workspace: &mut Workspace, + _: &OpenLocalSettings, + cx: &mut ViewContext, +) { + let project = workspace.project().clone(); + let worktree = project + .read(cx) + .visible_worktrees(cx) + .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); + if let Some(worktree) = worktree { + let tree_id = worktree.read(cx).id(); + cx.spawn(|workspace, mut cx| async move { + let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH; + + if let Some(dir_path) = file_path.parent() { + if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, dir_path), true, cx) + })? + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + } + + if worktree.update(&mut cx, |tree, _| tree.entry_for_path(file_path).is_none())? { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, file_path), false, cx) + })? + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + + let editor = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path((tree_id, file_path), None, true, cx) + })? + .await? + .downcast::() + .ok_or_else(|| anyhow!("unexpected item type"))?; + + editor + .downgrade() + .update(&mut cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + if buffer.read(cx).is_empty() { + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, initial_local_settings_content())], None, cx) + }); + } + } + }) + .ok(); + + anyhow::Ok(()) + }) + .detach(); + } else { + workspace.show_notification(0, cx, |cx| { + cx.build_view(|_| MessageNotification::new("This project has no folders open.")) + }) + } +} + +fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { + workspace.with_local_workspace(cx, move |workspace, cx| { + let app_state = workspace.app_state().clone(); + cx.spawn(|workspace, mut cx| async move { + async fn fetch_log_string(app_state: &Arc) -> Option { + let path = app_state.client.telemetry().log_file_path()?; + app_state.fs.load(&path).await.log_err() + } + + let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string()); + + const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024; + let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN); + if let Some(newline_offset) = log[start_offset..].find('\n') { + start_offset += newline_offset + 1; + } + let log_suffix = &log[start_offset..]; + let json = app_state.languages.language_for_name("JSON").await.log_err(); + + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| { + buffer.set_language(json, cx); + buffer.edit( + [( + 0..0, + concat!( + "// Zed collects anonymous usage data to help us understand how people are using the app.\n", + "// Telemetry can be disabled via the `settings.json` file.\n", + "// Here is the data that has been reported for the current session:\n", + "\n" + ), + )], + None, + cx, + ); + buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx); + }); + + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), + cx, + ); + }).log_err()?; + + Some(()) + }) + .detach(); + }).detach(); +} + +fn open_bundled_file( + workspace: &mut Workspace, + text: Cow<'static, str>, + title: &'static str, + language: &'static str, + cx: &mut ViewContext, +) { + let language = workspace.app_state().languages.language_for_name(language); + cx.spawn(|workspace, mut cx| async move { + let language = language.await.log_err(); + workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(cx, |workspace, cx| { + let project = workspace.project(); + let buffer = project.update(cx, move |project, cx| { + project + .create_buffer(text.as_ref(), language, cx) + .expect("creating buffers on a local workspace always succeeds") + }); + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title(title.into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| { + Editor::for_multibuffer(buffer, Some(project.clone()), cx) + })), + cx, + ); + }) + })? + .await + }) + .detach_and_log_err(cx); +} diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index 090352b2ccada82e4e5675384ac5b8a9bad8c90b..7f0c19853e2978b5bd63ee9abf00996ca1985812 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/crates/zed_actions2/src/lib.rs @@ -1,33 +1,19 @@ -use gpui::{action, actions}; +use gpui::action; -actions!( - About, - DebugElements, - DecreaseBufferFontSize, - Hide, - HideOthers, - IncreaseBufferFontSize, - Minimize, - OpenDefaultKeymap, - OpenDefaultSettings, - OpenKeymap, - OpenLicenses, - OpenLocalSettings, - OpenLog, - OpenSettings, - OpenTelemetryLog, - Quit, - ResetBufferFontSize, - ResetDatabase, - ShowAll, - ToggleFullScreen, - Zoom, -); +// If the zed binary doesn't use anything in this crate, it will be optimized away +// and the actions won't initialize. So we just provide an empty initialization function +// to be called from main. +// +// These may provide relevant context: +// https://github.com/rust-lang/rust/issues/47384 +// https://github.com/mmastrac/rust-ctor/issues/280 +pub fn init() {} #[action] pub struct OpenBrowser { pub url: String, } + #[action] pub struct OpenZedURL { pub url: String,