diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index bf9f9fa94b9691405f4ff9f682e6abcf9c6b0b18..e403a50cf9ffb087abe1579ee92b6ae64b779780 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -6,9 +6,12 @@ use gpui::{ WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; -use std::cmp::{self, Reverse}; +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; use theme::ActiveTheme; -use ui::{v_stack, Label, StyledExt}; +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -147,6 +150,10 @@ impl CommandPaletteDelegate { impl PickerDelegate for CommandPaletteDelegate { type ListItem = Div>; + fn placeholder_text(&self) -> Arc { + "Execute a command...".into() + } + fn match_count(&self) -> usize { self.matches.len() } @@ -296,11 +303,10 @@ impl PickerDelegate for CommandPaletteDelegate { cx: &mut ViewContext>, ) -> Self::ListItem { let colors = cx.theme().colors(); - let Some(command) = self - .matches - .get(ix) - .and_then(|m| self.commands.get(m.candidate_id)) - else { + let Some(r#match) = self.matches.get(ix) else { + return div(); + }; + let Some(command) = self.commands.get(r#match.candidate_id) else { return div(); }; @@ -312,63 +318,16 @@ impl PickerDelegate for CommandPaletteDelegate { .rounded_md() .when(selected, |this| this.bg(colors.ghost_element_selected)) .hover(|this| this.bg(colors.ghost_element_hover)) - .child(Label::new(command.name.clone())) + .child( + h_stack() + .justify_between() + .child(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) + .children(KeyBinding::for_action(&*command.action, cx)), + ) } - - // fn render_match( - // &self, - // ix: usize, - // mouse_state: &mut MouseState, - // selected: bool, - // cx: &gpui::AppContext, - // ) -> AnyElement> { - // let mat = &self.matches[ix]; - // let command = &self.actions[mat.candidate_id]; - // let theme = theme::current(cx); - // let style = theme.picker.item.in_state(selected).style_for(mouse_state); - // let key_style = &theme.command_palette.key.in_state(selected); - // let keystroke_spacing = theme.command_palette.keystroke_spacing; - - // Flex::row() - // .with_child( - // Label::new(mat.string.clone(), style.label.clone()) - // .with_highlights(mat.positions.clone()), - // ) - // .with_children(command.keystrokes.iter().map(|keystroke| { - // Flex::row() - // .with_children( - // [ - // (keystroke.ctrl, "^"), - // (keystroke.alt, "⌥"), - // (keystroke.cmd, "⌘"), - // (keystroke.shift, "⇧"), - // ] - // .into_iter() - // .filter_map(|(modifier, label)| { - // if modifier { - // Some( - // Label::new(label, key_style.label.clone()) - // .contained() - // .with_style(key_style.container), - // ) - // } else { - // None - // } - // }), - // ) - // .with_child( - // Label::new(keystroke.key.clone(), key_style.label.clone()) - // .contained() - // .with_style(key_style.container), - // ) - // .contained() - // .with_margin_left(keystroke_spacing) - // .flex_float() - // })) - // .contained() - // .with_style(style.container) - // .into_any() - // } } fn humanize_action_name(name: &str) -> String { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index e10cc9971eb9f7ee52c642430b0af393a12e6f10..20b970d6720ec890e69b54aa5330e6035a0cc538 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -41,8 +41,8 @@ use git::diff_hunk_to_display; use gpui::{ action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, - DispatchContext, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, - HighlightStyle, Hsla, InputHandler, Model, MouseButton, ParentElement, Pixels, Render, + EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, + InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -646,7 +646,7 @@ pub struct Editor { collapse_matches: bool, autoindent_mode: Option, workspace: Option<(WeakView, i64)>, - keymap_context_layers: BTreeMap, + keymap_context_layers: BTreeMap, input_enabled: bool, read_only: bool, leader_peer_id: Option, @@ -1981,9 +1981,9 @@ impl Editor { this } - fn dispatch_context(&self, cx: &AppContext) -> DispatchContext { - let mut dispatch_context = DispatchContext::default(); - dispatch_context.insert("Editor"); + fn dispatch_context(&self, cx: &AppContext) -> KeyContext { + let mut dispatch_context = KeyContext::default(); + dispatch_context.add("Editor"); let mode = match self.mode { EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", @@ -1991,17 +1991,17 @@ impl Editor { }; dispatch_context.set("mode", mode); if self.pending_rename.is_some() { - dispatch_context.insert("renaming"); + dispatch_context.add("renaming"); } if self.context_menu_visible() { match self.context_menu.read().as_ref() { Some(ContextMenu::Completions(_)) => { - dispatch_context.insert("menu"); - dispatch_context.insert("showing_completions") + dispatch_context.add("menu"); + dispatch_context.add("showing_completions") } Some(ContextMenu::CodeActions(_)) => { - dispatch_context.insert("menu"); - dispatch_context.insert("showing_code_actions") + dispatch_context.add("menu"); + dispatch_context.add("showing_code_actions") } None => {} } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index abbb06b3cab18679611e38bd9f5f8fb09493e38d..65f6edb18d547a7771e4fa828bb36059edcd0c7d 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -16,11 +16,11 @@ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, - BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchContext, DispatchPhase, - Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, - InputHandler, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, - Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, + BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, + ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, + KeyContext, KeyDownEvent, KeyMatch, Line, LineLayout, Modifiers, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, Style, TextRun, + TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -2459,11 +2459,170 @@ impl Element for EditorElement { let dispatch_context = editor.dispatch_context(cx); cx.with_element_id(cx.view().entity_id(), |global_id, cx| { - cx.with_key_dispatch_context(dispatch_context, |cx| { - cx.with_key_listeners(build_key_listeners(global_id), |cx| { - cx.with_focus(editor.focus_handle.clone(), |_| {}) - }); - }) + 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)); + }); + // on_action(cx, Editor::rename); todo!() + // on_action(cx, Editor::confirm_rename); todo!() + // on_action(cx, Editor::find_all_references); todo!() + 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); + }, + ) }); } @@ -3998,212 +4157,14 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } -fn build_key_listeners( - global_element_id: GlobalElementId, -) -> impl IntoIterator)> { - [ - build_action_listener(Editor::move_left), - build_action_listener(Editor::move_right), - build_action_listener(Editor::move_down), - build_action_listener(Editor::move_up), - // build_action_listener(Editor::new_file), todo!() - // build_action_listener(Editor::new_file_in_direction), todo!() - build_action_listener(Editor::cancel), - build_action_listener(Editor::newline), - build_action_listener(Editor::newline_above), - build_action_listener(Editor::newline_below), - build_action_listener(Editor::backspace), - build_action_listener(Editor::delete), - build_action_listener(Editor::tab), - build_action_listener(Editor::tab_prev), - build_action_listener(Editor::indent), - build_action_listener(Editor::outdent), - build_action_listener(Editor::delete_line), - build_action_listener(Editor::join_lines), - build_action_listener(Editor::sort_lines_case_sensitive), - build_action_listener(Editor::sort_lines_case_insensitive), - build_action_listener(Editor::reverse_lines), - build_action_listener(Editor::shuffle_lines), - build_action_listener(Editor::convert_to_upper_case), - build_action_listener(Editor::convert_to_lower_case), - build_action_listener(Editor::convert_to_title_case), - build_action_listener(Editor::convert_to_snake_case), - build_action_listener(Editor::convert_to_kebab_case), - build_action_listener(Editor::convert_to_upper_camel_case), - build_action_listener(Editor::convert_to_lower_camel_case), - build_action_listener(Editor::delete_to_previous_word_start), - build_action_listener(Editor::delete_to_previous_subword_start), - build_action_listener(Editor::delete_to_next_word_end), - build_action_listener(Editor::delete_to_next_subword_end), - build_action_listener(Editor::delete_to_beginning_of_line), - build_action_listener(Editor::delete_to_end_of_line), - build_action_listener(Editor::cut_to_end_of_line), - build_action_listener(Editor::duplicate_line), - build_action_listener(Editor::move_line_up), - build_action_listener(Editor::move_line_down), - build_action_listener(Editor::transpose), - build_action_listener(Editor::cut), - build_action_listener(Editor::copy), - build_action_listener(Editor::paste), - build_action_listener(Editor::undo), - build_action_listener(Editor::redo), - build_action_listener(Editor::move_page_up), - build_action_listener(Editor::move_page_down), - build_action_listener(Editor::next_screen), - build_action_listener(Editor::scroll_cursor_top), - build_action_listener(Editor::scroll_cursor_center), - build_action_listener(Editor::scroll_cursor_bottom), - build_action_listener(|editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }), - build_action_listener(|editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }), - build_action_listener(|editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }), - build_action_listener(|editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }), - build_action_listener(|editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }), - build_action_listener(|editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }), - build_action_listener(Editor::move_to_previous_word_start), - build_action_listener(Editor::move_to_previous_subword_start), - build_action_listener(Editor::move_to_next_word_end), - build_action_listener(Editor::move_to_next_subword_end), - build_action_listener(Editor::move_to_beginning_of_line), - build_action_listener(Editor::move_to_end_of_line), - build_action_listener(Editor::move_to_start_of_paragraph), - build_action_listener(Editor::move_to_end_of_paragraph), - build_action_listener(Editor::move_to_beginning), - build_action_listener(Editor::move_to_end), - build_action_listener(Editor::select_up), - build_action_listener(Editor::select_down), - build_action_listener(Editor::select_left), - build_action_listener(Editor::select_right), - build_action_listener(Editor::select_to_previous_word_start), - build_action_listener(Editor::select_to_previous_subword_start), - build_action_listener(Editor::select_to_next_word_end), - build_action_listener(Editor::select_to_next_subword_end), - build_action_listener(Editor::select_to_beginning_of_line), - build_action_listener(Editor::select_to_end_of_line), - build_action_listener(Editor::select_to_start_of_paragraph), - build_action_listener(Editor::select_to_end_of_paragraph), - build_action_listener(Editor::select_to_beginning), - build_action_listener(Editor::select_to_end), - build_action_listener(Editor::select_all), - build_action_listener(|editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }), - build_action_listener(Editor::select_line), - build_action_listener(Editor::split_selection_into_lines), - build_action_listener(Editor::add_selection_above), - build_action_listener(Editor::add_selection_below), - build_action_listener(|editor, action, cx| { - editor.select_next(action, cx).log_err(); - }), - build_action_listener(|editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }), - build_action_listener(Editor::toggle_comments), - build_action_listener(Editor::select_larger_syntax_node), - build_action_listener(Editor::select_smaller_syntax_node), - build_action_listener(Editor::move_to_enclosing_bracket), - build_action_listener(Editor::undo_selection), - build_action_listener(Editor::redo_selection), - build_action_listener(Editor::go_to_diagnostic), - build_action_listener(Editor::go_to_prev_diagnostic), - build_action_listener(Editor::go_to_hunk), - build_action_listener(Editor::go_to_prev_hunk), - build_action_listener(Editor::go_to_definition), - build_action_listener(Editor::go_to_definition_split), - build_action_listener(Editor::go_to_type_definition), - build_action_listener(Editor::go_to_type_definition_split), - build_action_listener(Editor::fold), - build_action_listener(Editor::fold_at), - build_action_listener(Editor::unfold_lines), - build_action_listener(Editor::unfold_at), - build_action_listener(Editor::fold_selected_ranges), - build_action_listener(Editor::show_completions), - build_action_listener(Editor::toggle_code_actions), - // build_action_listener(Editor::open_excerpts), todo!() - build_action_listener(Editor::toggle_soft_wrap), - build_action_listener(Editor::toggle_inlay_hints), - build_action_listener(Editor::reveal_in_finder), - build_action_listener(Editor::copy_path), - build_action_listener(Editor::copy_relative_path), - build_action_listener(Editor::copy_highlight_json), - build_action_listener(|editor, action, cx| { - editor - .format(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }), - build_action_listener(Editor::restart_language_server), - build_action_listener(Editor::show_character_palette), - // build_action_listener(Editor::confirm_completion), todo!() - build_action_listener(|editor, action, cx| { - editor - .confirm_code_action(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }), - // build_action_listener(Editor::rename), todo!() - // build_action_listener(Editor::confirm_rename), todo!() - // build_action_listener(Editor::find_all_references), todo!() - build_action_listener(Editor::next_copilot_suggestion), - build_action_listener(Editor::previous_copilot_suggestion), - build_action_listener(Editor::copilot_suggest), - build_action_listener(Editor::context_menu_first), - build_action_listener(Editor::context_menu_prev), - build_action_listener(Editor::context_menu_next), - build_action_listener(Editor::context_menu_last), - build_key_listener( - move |editor, key_down: &KeyDownEvent, dispatch_context, phase, cx| { - if phase == DispatchPhase::Bubble { - if let KeyMatch::Some(action) = cx.match_keystroke( - &global_element_id, - &key_down.keystroke, - dispatch_context, - ) { - return Some(action); - } - } - - None - }, - ), - ] -} - -fn build_key_listener( - listener: impl Fn( - &mut Editor, - &T, - &[&DispatchContext], - DispatchPhase, - &mut ViewContext, - ) -> Option> - + 'static, -) -> (TypeId, KeyListener) { - ( - TypeId::of::(), - Box::new(move |editor, event, dispatch_context, phase, cx| { - let key_event = event.downcast_ref::()?; - listener(editor, key_event, dispatch_context, phase, cx) - }), - ) -} - -fn build_action_listener( +fn register_action( + cx: &mut ViewContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, -) -> (TypeId, KeyListener) { - build_key_listener(move |editor, action: &T, dispatch_context, phase, cx| { +) { + cx.on_action(TypeId::of::(), move |editor, action, phase, cx| { + let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Bubble { listener(editor, action, cx); } - None }) } diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 25e9f91608fc857ea30191f25bbfc75aac6117ef..9614082ccf240601eb85a0bd9166f09a4db5e790 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -30,6 +30,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; +use ui::{Label, LabelColor}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::{ @@ -595,16 +596,19 @@ impl Item for Editor { .flex_row() .items_center() .gap_2() - .child(self.title(cx).to_string()) + .child(Label::new(self.title(cx).to_string())) .children(detail.and_then(|detail| { let path = path_for_buffer(&self.buffer, detail, false, cx)?; let description = path.to_string_lossy(); Some( - div() - .text_color(theme.colors().text_muted) - .text_xs() - .child(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)), + div().child( + Label::new(util::truncate_and_trailoff( + &description, + MAX_TAB_TITLE_LEN, + )) + .color(LabelColor::Muted), + ), ) })), ) diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 9ec770e05cdb73fb3b3ddc172f9949d5217ddabf..50592901b5396ca6cc11aa86b3f3c75820c0a2f1 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,8 +1,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, - StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext, - VisualContext, WindowContext, + StatelessInteractive, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; @@ -146,11 +145,11 @@ impl GoToLine { } impl Render for GoToLine { - type Element = Div>; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { modal(cx) - .id("go to line") + .context("GoToLine") .on_action(Self::cancel) .on_action(Self::confirm) .w_96() diff --git a/crates/gpui/src/dispatch.rs b/crates/gpui/src/dispatch.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/crates/gpui/src/dispatch.rs @@ -0,0 +1 @@ + diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 170ddf942f2bfcdacda710ef89094cd8aef726ec..6526f96cb9cbc0f5be445906cc55eb2801dc429d 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -1,6 +1,6 @@ use crate::SharedString; use anyhow::{anyhow, Context, Result}; -use collections::{HashMap, HashSet}; +use collections::HashMap; use lazy_static::lazy_static; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use serde::Deserialize; @@ -186,401 +186,3 @@ macro_rules! actions { actions!($($rest)*); }; } - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct DispatchContext { - set: HashSet, - map: HashMap, -} - -impl<'a> TryFrom<&'a str> for DispatchContext { - type Error = anyhow::Error; - - fn try_from(value: &'a str) -> Result { - Self::parse(value) - } -} - -impl DispatchContext { - pub fn parse(source: &str) -> Result { - let mut context = Self::default(); - let source = skip_whitespace(source); - Self::parse_expr(&source, &mut context)?; - Ok(context) - } - - fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> { - if source.is_empty() { - return Ok(()); - } - - let key = source - .chars() - .take_while(|c| is_identifier_char(*c)) - .collect::(); - source = skip_whitespace(&source[key.len()..]); - if let Some(suffix) = source.strip_prefix('=') { - source = skip_whitespace(suffix); - let value = source - .chars() - .take_while(|c| is_identifier_char(*c)) - .collect::(); - source = skip_whitespace(&source[value.len()..]); - context.set(key, value); - } else { - context.insert(key); - } - - Self::parse_expr(source, context) - } - - pub fn is_empty(&self) -> bool { - self.set.is_empty() && self.map.is_empty() - } - - pub fn clear(&mut self) { - self.set.clear(); - self.map.clear(); - } - - pub fn extend(&mut self, other: &Self) { - for v in &other.set { - self.set.insert(v.clone()); - } - for (k, v) in &other.map { - self.map.insert(k.clone(), v.clone()); - } - } - - pub fn insert>(&mut self, identifier: I) { - self.set.insert(identifier.into()); - } - - pub fn set, S2: Into>(&mut self, key: S1, value: S2) { - self.map.insert(key.into(), value.into()); - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum DispatchContextPredicate { - Identifier(SharedString), - Equal(SharedString, SharedString), - NotEqual(SharedString, SharedString), - Child(Box, Box), - Not(Box), - And(Box, Box), - Or(Box, Box), -} - -impl DispatchContextPredicate { - pub fn parse(source: &str) -> Result { - let source = skip_whitespace(source); - let (predicate, rest) = Self::parse_expr(source, 0)?; - if let Some(next) = rest.chars().next() { - Err(anyhow!("unexpected character {next:?}")) - } else { - Ok(predicate) - } - } - - pub fn eval(&self, contexts: &[&DispatchContext]) -> bool { - let Some(context) = contexts.last() else { - return false; - }; - match self { - Self::Identifier(name) => context.set.contains(name), - Self::Equal(left, right) => context - .map - .get(left) - .map(|value| value == right) - .unwrap_or(false), - Self::NotEqual(left, right) => context - .map - .get(left) - .map(|value| value != right) - .unwrap_or(true), - Self::Not(pred) => !pred.eval(contexts), - Self::Child(parent, child) => { - parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts) - } - Self::And(left, right) => left.eval(contexts) && right.eval(contexts), - Self::Or(left, right) => left.eval(contexts) || right.eval(contexts), - } - } - - fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { - type Op = fn( - DispatchContextPredicate, - DispatchContextPredicate, - ) -> Result; - - let (mut predicate, rest) = Self::parse_primary(source)?; - source = rest; - - 'parse: loop { - for (operator, precedence, constructor) in [ - (">", PRECEDENCE_CHILD, Self::new_child as Op), - ("&&", PRECEDENCE_AND, Self::new_and as Op), - ("||", PRECEDENCE_OR, Self::new_or as Op), - ("==", PRECEDENCE_EQ, Self::new_eq as Op), - ("!=", PRECEDENCE_EQ, Self::new_neq as Op), - ] { - if source.starts_with(operator) && precedence >= min_precedence { - source = skip_whitespace(&source[operator.len()..]); - let (right, rest) = Self::parse_expr(source, precedence + 1)?; - predicate = constructor(predicate, right)?; - source = rest; - continue 'parse; - } - } - break; - } - - Ok((predicate, source)) - } - - fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { - let next = source - .chars() - .next() - .ok_or_else(|| anyhow!("unexpected eof"))?; - match next { - '(' => { - source = skip_whitespace(&source[1..]); - let (predicate, rest) = Self::parse_expr(source, 0)?; - if rest.starts_with(')') { - source = skip_whitespace(&rest[1..]); - Ok((predicate, source)) - } else { - Err(anyhow!("expected a ')'")) - } - } - '!' => { - let source = skip_whitespace(&source[1..]); - let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?; - Ok((DispatchContextPredicate::Not(Box::new(predicate)), source)) - } - _ if is_identifier_char(next) => { - let len = source - .find(|c: char| !is_identifier_char(c)) - .unwrap_or(source.len()); - let (identifier, rest) = source.split_at(len); - source = skip_whitespace(rest); - Ok(( - DispatchContextPredicate::Identifier(identifier.to_string().into()), - source, - )) - } - _ => Err(anyhow!("unexpected character {next:?}")), - } - } - - fn new_or(self, other: Self) -> Result { - Ok(Self::Or(Box::new(self), Box::new(other))) - } - - fn new_and(self, other: Self) -> Result { - Ok(Self::And(Box::new(self), Box::new(other))) - } - - fn new_child(self, other: Self) -> Result { - Ok(Self::Child(Box::new(self), Box::new(other))) - } - - fn new_eq(self, other: Self) -> Result { - if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { - Ok(Self::Equal(left, right)) - } else { - Err(anyhow!("operands must be identifiers")) - } - } - - fn new_neq(self, other: Self) -> Result { - if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { - Ok(Self::NotEqual(left, right)) - } else { - Err(anyhow!("operands must be identifiers")) - } - } -} - -const PRECEDENCE_CHILD: u32 = 1; -const PRECEDENCE_OR: u32 = 2; -const PRECEDENCE_AND: u32 = 3; -const PRECEDENCE_EQ: u32 = 4; -const PRECEDENCE_NOT: u32 = 5; - -fn is_identifier_char(c: char) -> bool { - c.is_alphanumeric() || c == '_' || c == '-' -} - -fn skip_whitespace(source: &str) -> &str { - let len = source - .find(|c: char| !c.is_whitespace()) - .unwrap_or(source.len()); - &source[len..] -} - -#[cfg(test)] -mod tests { - use super::*; - use crate as gpui; - use DispatchContextPredicate::*; - - #[test] - fn test_actions_definition() { - { - actions!(A, B, C, D, E, F, G); - } - - { - actions!( - A, - B, - C, - D, - E, - F, - G, // Don't wrap, test the trailing comma - ); - } - } - - #[test] - fn test_parse_context() { - let mut expected = DispatchContext::default(); - expected.set("foo", "bar"); - expected.insert("baz"); - assert_eq!(DispatchContext::parse("baz foo=bar").unwrap(), expected); - assert_eq!(DispatchContext::parse("foo = bar baz").unwrap(), expected); - assert_eq!( - DispatchContext::parse(" baz foo = bar baz").unwrap(), - expected - ); - assert_eq!(DispatchContext::parse(" foo = bar baz").unwrap(), expected); - } - - #[test] - fn test_parse_identifiers() { - // Identifiers - assert_eq!( - DispatchContextPredicate::parse("abc12").unwrap(), - Identifier("abc12".into()) - ); - assert_eq!( - DispatchContextPredicate::parse("_1a").unwrap(), - Identifier("_1a".into()) - ); - } - - #[test] - fn test_parse_negations() { - assert_eq!( - DispatchContextPredicate::parse("!abc").unwrap(), - Not(Box::new(Identifier("abc".into()))) - ); - assert_eq!( - DispatchContextPredicate::parse(" ! ! abc").unwrap(), - Not(Box::new(Not(Box::new(Identifier("abc".into()))))) - ); - } - - #[test] - fn test_parse_equality_operators() { - assert_eq!( - DispatchContextPredicate::parse("a == b").unwrap(), - Equal("a".into(), "b".into()) - ); - assert_eq!( - DispatchContextPredicate::parse("c!=d").unwrap(), - NotEqual("c".into(), "d".into()) - ); - assert_eq!( - DispatchContextPredicate::parse("c == !d") - .unwrap_err() - .to_string(), - "operands must be identifiers" - ); - } - - #[test] - fn test_parse_boolean_operators() { - assert_eq!( - DispatchContextPredicate::parse("a || b").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - ) - ); - assert_eq!( - DispatchContextPredicate::parse("a || !b && c").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(And( - Box::new(Not(Box::new(Identifier("b".into())))), - Box::new(Identifier("c".into())) - )) - ) - ); - assert_eq!( - DispatchContextPredicate::parse("a && b || c&&d").unwrap(), - Or( - Box::new(And( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - )), - Box::new(And( - Box::new(Identifier("c".into())), - Box::new(Identifier("d".into())) - )) - ) - ); - assert_eq!( - DispatchContextPredicate::parse("a == b && c || d == e && f").unwrap(), - Or( - Box::new(And( - Box::new(Equal("a".into(), "b".into())), - Box::new(Identifier("c".into())) - )), - Box::new(And( - Box::new(Equal("d".into(), "e".into())), - Box::new(Identifier("f".into())) - )) - ) - ); - assert_eq!( - DispatchContextPredicate::parse("a && b && c && d").unwrap(), - And( - Box::new(And( - Box::new(And( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())) - )), - Box::new(Identifier("c".into())), - )), - Box::new(Identifier("d".into())) - ), - ); - } - - #[test] - fn test_parse_parenthesized_expressions() { - assert_eq!( - DispatchContextPredicate::parse("a && (b == c || d != e)").unwrap(), - And( - Box::new(Identifier("a".into())), - Box::new(Or( - Box::new(Equal("b".into(), "c".into())), - Box::new(NotEqual("d".into(), "e".into())), - )), - ), - ); - assert_eq!( - DispatchContextPredicate::parse(" ( a || b ) ").unwrap(), - Or( - Box::new(Identifier("a".into())), - Box::new(Identifier("b".into())), - ) - ); - } -} diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index a56a8c24d4e07d6efc0cd42c0e87e90fada6a367..b6cb3f6307b55d38d2fa7fc12fb346447b8b8cfd 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -641,14 +641,19 @@ impl AppContext { // The window might change focus multiple times in an effect cycle. // We only honor effects for the most recently focused handle. if cx.window.focus == focused { + // if someone calls focus multiple times in one frame with the same handle + // the first apply_focus_changed_effect will have taken the last blur already + // and run the rest of this, so we can return. + let Some(last_blur) = cx.window.last_blur.take() else { + return; + }; + let focused = focused .map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap()); - let blurred = cx - .window - .last_blur - .take() - .unwrap() - .and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles)); + + let blurred = + last_blur.and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles)); + let focus_changed = focused.is_some() || blurred.is_some(); let event = FocusEvent { focused, blurred }; diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index d88a4119b7232838042ebe53f984b9468e1309c0..95c44038ed115ead27a00d2dcd7a10e1fae429dc 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,29 +1,33 @@ +use std::fmt::Debug; + use crate::{ - point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementFocus, ElementId, - ElementInteractivity, FocusDisabled, FocusEnabled, FocusHandle, FocusListeners, Focusable, - GlobalElementId, GroupBounds, InteractiveElementState, LayoutId, Overflow, ParentElement, - Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, - StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext, Visibility, + 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, }; use refineable::Refineable; use smallvec::SmallVec; +use util::ResultExt; pub struct Div< V: 'static, I: ElementInteractivity = StatelessInteractivity, - F: ElementFocus = FocusDisabled, + K: KeyDispatch = NonFocusableKeyDispatch, > { interactivity: I, - focus: F, + key_dispatch: K, children: SmallVec<[AnyElement; 2]>, group: Option, base_style: StyleRefinement, } -pub fn div() -> Div, FocusDisabled> { +pub fn div() -> Div, NonFocusableKeyDispatch> { Div { interactivity: StatelessInteractivity::default(), - focus: FocusDisabled, + key_dispatch: NonFocusableKeyDispatch::default(), children: SmallVec::new(), group: None, base_style: StyleRefinement::default(), @@ -33,12 +37,12 @@ pub fn div() -> Div, FocusDisabled> { impl Div, F> where V: 'static, - F: ElementFocus, + F: KeyDispatch, { pub fn id(self, id: impl Into) -> Div, F> { Div { interactivity: StatefulInteractivity::new(id.into(), self.interactivity), - focus: self.focus, + key_dispatch: self.key_dispatch, children: self.children, group: self.group, base_style: self.base_style, @@ -49,7 +53,7 @@ where impl Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { pub fn group(mut self, group: impl Into) -> Self { self.group = Some(group.into()); @@ -61,6 +65,18 @@ where self } + pub fn context(mut self, context: C) -> 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 + } + pub fn overflow_hidden(mut self) -> Self { self.base_style.overflow.x = Some(Overflow::Hidden); self.base_style.overflow.y = Some(Overflow::Hidden); @@ -97,7 +113,7 @@ where ) -> Style { let mut computed_style = Style::default(); computed_style.refine(&self.base_style); - self.focus.refine_style(&mut computed_style, cx); + self.key_dispatch.refine_style(&mut computed_style, cx); self.interactivity.refine_style( &mut computed_style, bounds, @@ -108,11 +124,11 @@ where } } -impl Div, FocusDisabled> { - pub fn focusable(self) -> Div, FocusEnabled> { +impl Div, NonFocusableKeyDispatch> { + pub fn focusable(self) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity, - focus: FocusEnabled::new(), + key_dispatch: FocusableKeyDispatch::new(self.key_dispatch), children: self.children, group: self.group, base_style: self.base_style, @@ -122,10 +138,10 @@ impl Div, FocusDisabled> { pub fn track_focus( self, handle: &FocusHandle, - ) -> Div, FocusEnabled> { + ) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity, - focus: FocusEnabled::tracked(handle), + key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), children: self.children, group: self.group, base_style: self.base_style, @@ -149,14 +165,14 @@ impl Div, FocusDisabled> { } } -impl Div, FocusDisabled> { +impl Div, NonFocusableKeyDispatch> { pub fn track_focus( self, handle: &FocusHandle, - ) -> Div, FocusEnabled> { + ) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity.into_stateful(handle), - focus: handle.clone().into(), + key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), children: self.children, group: self.group, base_style: self.base_style, @@ -164,25 +180,25 @@ impl Div, FocusDisabled> { } } -impl Focusable for Div> +impl Focusable for Div> where V: 'static, I: ElementInteractivity, { fn focus_listeners(&mut self) -> &mut FocusListeners { - &mut self.focus.focus_listeners + &mut self.key_dispatch.focus_listeners } fn set_focus_style(&mut self, style: StyleRefinement) { - self.focus.focus_style = style; + self.key_dispatch.focus_style = style; } fn set_focus_in_style(&mut self, style: StyleRefinement) { - self.focus.focus_in_style = style; + self.key_dispatch.focus_in_style = style; } fn set_in_focus_style(&mut self, style: StyleRefinement) { - self.focus.in_focus_style = style; + self.key_dispatch.in_focus_style = style; } } @@ -196,7 +212,7 @@ pub struct DivState { impl Element for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { type ElementState = DivState; @@ -213,14 +229,18 @@ where cx: &mut ViewContext, ) -> Self::ElementState { let mut element_state = element_state.unwrap_or_default(); - self.interactivity.initialize(cx, |cx| { - self.focus - .initialize(element_state.focus_handle.take(), cx, |focus_handle, cx| { + 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 self.children { + for child in &mut this.children { child.initialize(view_state, cx); } - }) + }, + ); }); element_state } @@ -288,7 +308,7 @@ where cx.with_z_index(z_index, |cx| { cx.with_z_index(0, |cx| { style.paint(bounds, cx); - this.focus.paint(bounds, cx); + this.key_dispatch.paint(bounds, cx); this.interactivity.paint( bounds, content_size, @@ -321,7 +341,7 @@ where impl Component for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn render(self) -> AnyElement { AnyElement::new(self) @@ -331,7 +351,7 @@ where impl ParentElement for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children @@ -341,7 +361,7 @@ where impl Styled for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn style(&mut self) -> &mut StyleRefinement { &mut self.base_style @@ -351,7 +371,7 @@ where impl StatelessInteractive for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { self.interactivity.as_stateless_mut() @@ -360,7 +380,7 @@ where impl StatefulInteractive for Div, F> where - F: ElementFocus, + F: KeyDispatch, { fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { &mut self.interactivity diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 638665d4141d4a19362b2812932048a8ad46c348..1ff088c1afc4a2098dc2ba41e37d0ae2ff6eef55 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,7 +1,7 @@ use crate::{ - div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus, - ElementId, ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, - LayoutId, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, + 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, }; use futures::FutureExt; @@ -10,14 +10,14 @@ use util::ResultExt; pub struct Img< V: 'static, I: ElementInteractivity = StatelessInteractivity, - F: ElementFocus = FocusDisabled, + F: KeyDispatch = NonFocusableKeyDispatch, > { base: Div, uri: Option, grayscale: bool, } -pub fn img() -> Img, FocusDisabled> { +pub fn img() -> Img, NonFocusableKeyDispatch> { Img { base: div(), uri: None, @@ -29,7 +29,7 @@ impl Img where V: 'static, I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { pub fn uri(mut self, uri: impl Into) -> Self { self.uri = Some(uri.into()); @@ -44,7 +44,7 @@ where impl Img, F> where - F: ElementFocus, + F: KeyDispatch, { pub fn id(self, id: impl Into) -> Img, F> { Img { @@ -58,7 +58,7 @@ where impl Component for Img where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn render(self) -> AnyElement { AnyElement::new(self) @@ -68,7 +68,7 @@ where impl Element for Img where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { type ElementState = DivState; @@ -137,7 +137,7 @@ where impl Styled for Img where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn style(&mut self) -> &mut StyleRefinement { self.base.style() @@ -147,7 +147,7 @@ where impl StatelessInteractive for Img where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { self.base.stateless_interactivity() @@ -156,14 +156,14 @@ where impl StatefulInteractive for Img, F> where - F: ElementFocus, + F: KeyDispatch, { fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { self.base.stateful_interactivity() } } -impl Focusable for Img> +impl Focusable for Img> where V: 'static, I: ElementInteractivity, diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 8e2ba9d8a1ba944f7cd82ca583e30e53de147c23..bafedb7f2d8469f4c143b59d6f8c743710ebef8e 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,21 +1,21 @@ use crate::{ - div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId, - ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels, - SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, - StatelessInteractivity, StyleRefinement, Styled, ViewContext, + div, AnyElement, Bounds, Component, Div, DivState, Element, ElementId, ElementInteractivity, + FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId, + NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, + StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext, }; use util::ResultExt; pub struct Svg< V: 'static, I: ElementInteractivity = StatelessInteractivity, - F: ElementFocus = FocusDisabled, + F: KeyDispatch = NonFocusableKeyDispatch, > { base: Div, path: Option, } -pub fn svg() -> Svg, FocusDisabled> { +pub fn svg() -> Svg, NonFocusableKeyDispatch> { Svg { base: div(), path: None, @@ -25,7 +25,7 @@ pub fn svg() -> Svg, FocusDisabled> { impl Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { pub fn path(mut self, path: impl Into) -> Self { self.path = Some(path.into()); @@ -35,7 +35,7 @@ where impl Svg, F> where - F: ElementFocus, + F: KeyDispatch, { pub fn id(self, id: impl Into) -> Svg, F> { Svg { @@ -48,7 +48,7 @@ where impl Component for Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn render(self) -> AnyElement { AnyElement::new(self) @@ -58,7 +58,7 @@ where impl Element for Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { type ElementState = DivState; @@ -108,7 +108,7 @@ where impl Styled for Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn style(&mut self) -> &mut StyleRefinement { self.base.style() @@ -118,7 +118,7 @@ where impl StatelessInteractive for Svg where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity { self.base.stateless_interactivity() @@ -128,14 +128,14 @@ where impl StatefulInteractive for Svg, F> where V: 'static, - F: ElementFocus, + F: KeyDispatch, { fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity { self.base.stateful_interactivity() } } -impl Focusable for Svg> +impl Focusable for Svg> where I: ElementInteractivity, { diff --git a/crates/gpui2/src/focusable.rs b/crates/gpui2/src/focusable.rs deleted file mode 100644 index 99f8bb1dd612631ad3dde3c97c1675f5072524bc..0000000000000000000000000000000000000000 --- a/crates/gpui2/src/focusable.rs +++ /dev/null @@ -1,252 +0,0 @@ -use crate::{ - Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, MouseDownEvent, Pixels, Style, - StyleRefinement, ViewContext, WindowContext, -}; -use refineable::Refineable; -use smallvec::SmallVec; - -pub type FocusListeners = SmallVec<[FocusListener; 2]>; - -pub type FocusListener = - Box) + 'static>; - -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 - } -} - -pub trait ElementFocus: 'static { - fn as_focusable(&self) -> Option<&FocusEnabled>; - fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled>; - - fn initialize( - &mut self, - focus_handle: Option, - cx: &mut ViewContext, - f: impl FnOnce(Option, &mut ViewContext) -> R, - ) -> R { - 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) - }); - } - cx.with_focus(focus_handle.clone(), |cx| f(Some(focus_handle), cx)) - } else { - f(None, cx) - } - } - - 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 FocusEnabled { - pub focus_handle: Option, - pub focus_listeners: FocusListeners, - pub focus_style: StyleRefinement, - pub focus_in_style: StyleRefinement, - pub in_focus_style: StyleRefinement, -} - -impl FocusEnabled { - pub fn new() -> Self { - Self { - focus_handle: None, - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } - - pub fn tracked(handle: &FocusHandle) -> Self { - Self { - focus_handle: Some(handle.clone()), - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } -} - -impl ElementFocus for FocusEnabled { - fn as_focusable(&self) -> Option<&FocusEnabled> { - Some(self) - } - - fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled> { - Some(self) - } -} - -impl From for FocusEnabled { - fn from(value: FocusHandle) -> Self { - Self { - focus_handle: Some(value), - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } - } -} - -pub struct FocusDisabled; - -impl ElementFocus for FocusDisabled { - fn as_focusable(&self) -> Option<&FocusEnabled> { - None - } - - fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled> { - None - } -} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index ef6c4a9e156010f0758cf69df78e8383d14951c0..b9747edabdf04327fa4c655b63483b448a7724b2 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -6,13 +6,14 @@ mod color; mod element; mod elements; mod executor; -mod focusable; mod geometry; mod image_cache; mod input; mod interactive; +mod key_dispatch; mod keymap; mod platform; +pub mod prelude; mod scene; mod style; mod styled; @@ -41,12 +42,12 @@ pub use ctor::ctor; pub use element::*; pub use elements::*; pub use executor::*; -pub use focusable::*; pub use geometry::*; pub use gpui2_macros::*; pub use image_cache::*; pub use input::*; pub use interactive::*; +pub use key_dispatch::*; pub use keymap::*; pub use platform::*; use private::Sealed; diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 243eb3cb07844a2fa558d6c1bf2555f75cf1af95..aacaeac01ff0dd60dfc096d04a82fb2029704874 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,8 +1,8 @@ use crate::{ - div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds, - Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, - Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, - StyleRefinement, Task, View, ViewContext, + 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, + ViewContext, }; use collections::HashMap; use derive_more::{Deref, DerefMut}; @@ -164,55 +164,41 @@ pub trait StatelessInteractive: Element { self } - fn context(mut self, context: C) -> Self - where - Self: Sized, - C: TryInto, - C::Error: Debug, - { - self.stateless_interactivity().dispatch_context = - context.try_into().expect("invalid dispatch context"); - self - } - /// Capture the given action, fires during the capture phase - fn capture_action( + fn capture_action( mut self, listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, ) -> Self where Self: Sized, { - self.stateless_interactivity().key_listeners.push(( + self.stateless_interactivity().action_listeners.push(( TypeId::of::(), - Box::new(move |view, action, _dipatch_context, phase, cx| { + Box::new(move |view, action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Capture { listener(view, action, cx) } - None }), )); self } /// Add a listener for the given action, fires during the bubble event phase - fn on_action( + fn on_action( mut self, listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, ) -> Self where Self: Sized, { - self.stateless_interactivity().key_listeners.push(( + self.stateless_interactivity().action_listeners.push(( TypeId::of::(), - Box::new(move |view, action, _dispatch_context, phase, cx| { + Box::new(move |view, action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Bubble { listener(view, action, cx) } - - None }), )); self @@ -225,14 +211,11 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interactivity().key_listeners.push(( - TypeId::of::(), - Box::new(move |view, event, _, phase, cx| { - let event = event.downcast_ref().unwrap(); - listener(view, event, phase, cx); - None - }), - )); + self.stateless_interactivity() + .key_down_listeners + .push(Box::new(move |view, event, phase, cx| { + listener(view, event, phase, cx) + })); self } @@ -243,14 +226,11 @@ pub trait StatelessInteractive: Element { where Self: Sized, { - self.stateless_interactivity().key_listeners.push(( - TypeId::of::(), - Box::new(move |view, event, _, phase, cx| { - let event = event.downcast_ref().unwrap(); - listener(view, event, phase, cx); - None - }), - )); + self.stateless_interactivity() + .key_up_listeners + .push(Box::new(move |view, event, phase, cx| { + listener(view, event, phase, cx) + })); self } @@ -396,43 +376,6 @@ pub trait ElementInteractivity: 'static { fn as_stateful(&self) -> Option<&StatefulInteractivity>; fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity>; - fn initialize( - &mut self, - cx: &mut ViewContext, - f: impl FnOnce(&mut ViewContext) -> R, - ) -> R { - if let Some(stateful) = self.as_stateful_mut() { - cx.with_element_id(stateful.id.clone(), |global_id, cx| { - // In addition to any key down/up listeners registered directly on the element, - // we also add a key listener to match actions from the keymap. - stateful.key_listeners.push(( - TypeId::of::(), - Box::new(move |_, key_down, context, phase, cx| { - if phase == DispatchPhase::Bubble { - let key_down = key_down.downcast_ref::().unwrap(); - if let KeyMatch::Some(action) = - cx.match_keystroke(&global_id, &key_down.keystroke, context) - { - return Some(action); - } - } - - None - }), - )); - - cx.with_key_dispatch_context(stateful.dispatch_context.clone(), |cx| { - cx.with_key_listeners(mem::take(&mut stateful.key_listeners), f) - }) - }) - } else { - let stateless = self.as_stateless_mut(); - cx.with_key_dispatch_context(stateless.dispatch_context.clone(), |cx| { - cx.with_key_listeners(mem::take(&mut stateless.key_listeners), f) - }) - } - } - fn refine_style( &self, style: &mut Style, @@ -487,6 +430,26 @@ pub trait ElementInteractivity: 'static { } } + 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, @@ -808,12 +771,14 @@ impl ElementInteractivity for StatefulInteractivity { type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; pub struct StatelessInteractivity { - pub dispatch_context: DispatchContext, + 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_listeners: SmallVec<[(TypeId, KeyListener); 32]>, + 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]>, @@ -910,12 +875,14 @@ impl InteractiveElementState { impl Default for StatelessInteractivity { fn default() -> Self { Self { - dispatch_context: DispatchContext::default(), + 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_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(), @@ -1250,16 +1217,14 @@ pub(crate) type HoverListener = Box) pub(crate) type TooltipBuilder = Arc) -> AnyView + 'static>; -pub type KeyListener = Box< - dyn Fn( - &mut V, - &dyn Any, - &[&DispatchContext], - DispatchPhase, - &mut ViewContext, - ) -> Option> - + 'static, ->; +pub(crate) type KeyDownListener = + Box) + 'static>; + +pub(crate) type KeyUpListener = + Box) + 'static>; + +pub type ActionListener = + Box) + 'static>; #[cfg(test)] mod test { @@ -1282,9 +1247,10 @@ mod test { fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { div().id("testview").child( div() + .context("test") + .track_focus(&self.focus_handle) .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) - .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true) - .track_focus(&self.focus_handle), + .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true), ) } } diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs new file mode 100644 index 0000000000000000000000000000000000000000..323fd7d2ff323c8606142b6182cf829961b15cff --- /dev/null +++ b/crates/gpui2/src/key_dispatch.rs @@ -0,0 +1,465 @@ +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, +}; +use collections::HashMap; +use parking_lot::Mutex; +use refineable::Refineable; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + rc::Rc, + sync::Arc, +}; +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); + +pub(crate) struct DispatchTree { + node_stack: Vec, + context_stack: Vec, + nodes: Vec, + focusable_node_ids: HashMap, + keystroke_matchers: HashMap, KeystrokeMatcher>, + keymap: Arc>, +} + +#[derive(Default)] +pub(crate) struct DispatchNode { + pub key_listeners: SmallVec<[KeyListener; 2]>, + pub action_listeners: SmallVec<[DispatchActionListener; 16]>, + pub context: KeyContext, + parent: Option, +} + +type KeyListener = Rc; + +#[derive(Clone)] +pub(crate) struct DispatchActionListener { + pub(crate) action_type: TypeId, + pub(crate) listener: Rc, +} + +impl DispatchTree { + pub fn new(keymap: Arc>) -> Self { + Self { + node_stack: Vec::new(), + context_stack: Vec::new(), + nodes: Vec::new(), + focusable_node_ids: HashMap::default(), + keystroke_matchers: HashMap::default(), + keymap, + } + } + + pub fn clear(&mut self) { + self.node_stack.clear(); + self.nodes.clear(); + self.context_stack.clear(); + self.focusable_node_ids.clear(); + self.keystroke_matchers.clear(); + } + + pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) { + let parent = self.node_stack.last().copied(); + let node_id = DispatchNodeId(self.nodes.len()); + self.nodes.push(DispatchNode { + parent, + ..Default::default() + }); + self.node_stack.push(node_id); + if !context.is_empty() { + self.active_node().context = context.clone(); + self.context_stack.push(context); + if let Some((context_stack, matcher)) = old_dispatcher + .keystroke_matchers + .remove_entry(self.context_stack.as_slice()) + { + self.keystroke_matchers.insert(context_stack, matcher); + } + } + } + + pub fn pop_node(&mut self) { + let node_id = self.node_stack.pop().unwrap(); + if !self.nodes[node_id.0].context.is_empty() { + self.context_stack.pop(); + } + } + + pub fn on_key_event(&mut self, listener: KeyListener) { + self.active_node().key_listeners.push(listener); + } + + pub fn on_action( + &mut self, + action_type: TypeId, + listener: Rc, + ) { + self.active_node() + .action_listeners + .push(DispatchActionListener { + action_type, + listener, + }); + } + + pub fn make_focusable(&mut self, focus_id: FocusId) { + self.focusable_node_ids + .insert(focus_id, self.active_node_id()); + } + + pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool { + if parent == child { + return true; + } + + if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) { + let mut current_node_id = self.focusable_node_ids.get(&child).copied(); + while let Some(node_id) = current_node_id { + if node_id == *parent_node_id { + return true; + } + current_node_id = self.nodes[node_id.0].parent; + } + } + false + } + + pub fn available_actions(&self, target: FocusId) -> Vec> { + let mut actions = Vec::new(); + if let Some(node) = self.focusable_node_ids.get(&target) { + for node_id in self.dispatch_path(*node) { + let node = &self.nodes[node_id.0]; + for DispatchActionListener { action_type, .. } in &node.action_listeners { + actions.extend(build_action_from_type(action_type).log_err()); + } + } + } + actions + } + + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.keymap + .lock() + .bindings_for_action(action.type_id()) + .filter(|candidate| candidate.action.partial_eq(action)) + .cloned() + .collect() + } + + pub fn dispatch_key( + &mut self, + keystroke: &Keystroke, + context: &[KeyContext], + ) -> Option> { + if !self.keystroke_matchers.contains_key(context) { + let keystroke_contexts = context.iter().cloned().collect(); + self.keystroke_matchers.insert( + keystroke_contexts, + KeystrokeMatcher::new(self.keymap.clone()), + ); + } + + let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap(); + if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(keystroke, context) { + // Clear all pending keystrokes when an action has been found. + for keystroke_matcher in self.keystroke_matchers.values_mut() { + keystroke_matcher.clear_pending(); + } + + Some(action) + } else { + None + } + } + + pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { + let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new(); + let mut current_node_id = Some(target); + while let Some(node_id) = current_node_id { + dispatch_path.push(node_id); + current_node_id = self.nodes[node_id.0].parent; + } + dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. + dispatch_path + } + + pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { + &self.nodes[node_id.0] + } + + fn active_node(&mut self) -> &mut DispatchNode { + let active_node_id = self.active_node_id(); + &mut self.nodes[active_node_id.0] + } + + pub fn focusable_node_id(&self, target: FocusId) -> Option { + self.focusable_node_ids.get(&target).copied() + } + + fn active_node_id(&self) -> DispatchNodeId { + *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/binding.rs b/crates/gpui2/src/keymap/binding.rs index 829f7a3b2cfa7a816b76ebc7c1acd3229b57aa18..e55d664610c2ffb90f402016f8589de2f98c8f1d 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -1,11 +1,21 @@ -use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke}; +use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke}; use anyhow::Result; use smallvec::SmallVec; pub struct KeyBinding { - action: Box, - pub(super) keystrokes: SmallVec<[Keystroke; 2]>, - pub(super) context_predicate: Option, + pub(crate) action: Box, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, + pub(crate) context_predicate: Option, +} + +impl Clone for KeyBinding { + fn clone(&self) -> Self { + KeyBinding { + action: self.action.boxed_clone(), + keystrokes: self.keystrokes.clone(), + context_predicate: self.context_predicate.clone(), + } + } } impl KeyBinding { @@ -15,7 +25,7 @@ impl KeyBinding { pub fn load(keystrokes: &str, action: Box, context: Option<&str>) -> Result { let context = if let Some(context) = context { - Some(DispatchContextPredicate::parse(context)?) + Some(KeyBindingContextPredicate::parse(context)?) } else { None }; @@ -32,7 +42,7 @@ impl KeyBinding { }) } - pub fn matches_context(&self, contexts: &[&DispatchContext]) -> bool { + pub fn matches_context(&self, contexts: &[KeyContext]) -> bool { self.context_predicate .as_ref() .map(|predicate| predicate.eval(contexts)) @@ -42,7 +52,7 @@ impl KeyBinding { pub fn match_keystrokes( &self, pending_keystrokes: &[Keystroke], - contexts: &[&DispatchContext], + contexts: &[KeyContext], ) -> KeyMatch { if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) @@ -61,7 +71,7 @@ impl KeyBinding { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[&DispatchContext], + contexts: &[KeyContext], ) -> Option> { if self.action.partial_eq(action) && self.matches_context(contexts) { Some(self.keystrokes.clone()) diff --git a/crates/gpui2/src/keymap/context.rs b/crates/gpui2/src/keymap/context.rs new file mode 100644 index 0000000000000000000000000000000000000000..b9cb0384ecc6036fa9ac72bd9458987a65ca53a2 --- /dev/null +++ b/crates/gpui2/src/keymap/context.rs @@ -0,0 +1,449 @@ +use crate::SharedString; +use anyhow::{anyhow, Result}; +use smallvec::SmallVec; +use std::fmt; + +#[derive(Clone, Default, Eq, PartialEq, Hash)] +pub struct KeyContext(SmallVec<[ContextEntry; 8]>); + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +struct ContextEntry { + key: SharedString, + value: Option, +} + +impl<'a> TryFrom<&'a str> for KeyContext { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + Self::parse(value) + } +} + +impl KeyContext { + pub fn parse(source: &str) -> Result { + let mut context = Self::default(); + let source = skip_whitespace(source); + Self::parse_expr(&source, &mut context)?; + Ok(context) + } + + fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> { + if source.is_empty() { + return Ok(()); + } + + let key = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[key.len()..]); + if let Some(suffix) = source.strip_prefix('=') { + source = skip_whitespace(suffix); + let value = source + .chars() + .take_while(|c| is_identifier_char(*c)) + .collect::(); + source = skip_whitespace(&source[value.len()..]); + context.set(key, value); + } else { + context.add(key); + } + + Self::parse_expr(source, context) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn clear(&mut self) { + self.0.clear(); + } + + pub fn extend(&mut self, other: &Self) { + for entry in &other.0 { + if !self.contains(&entry.key) { + self.0.push(entry.clone()); + } + } + } + + pub fn add>(&mut self, identifier: I) { + let key = identifier.into(); + + if !self.contains(&key) { + self.0.push(ContextEntry { key, value: None }) + } + } + + pub fn set, S2: Into>(&mut self, key: S1, value: S2) { + let key = key.into(); + if !self.contains(&key) { + self.0.push(ContextEntry { + key, + value: Some(value.into()), + }) + } + } + + pub fn contains(&self, key: &str) -> bool { + self.0.iter().any(|entry| entry.key.as_ref() == key) + } + + pub fn get(&self, key: &str) -> Option<&SharedString> { + self.0 + .iter() + .find(|entry| entry.key.as_ref() == key)? + .value + .as_ref() + } +} + +impl fmt::Debug for KeyContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut entries = self.0.iter().peekable(); + while let Some(entry) = entries.next() { + if let Some(ref value) = entry.value { + write!(f, "{}={}", entry.key, value)?; + } else { + write!(f, "{}", entry.key)?; + } + if entries.peek().is_some() { + write!(f, " ")?; + } + } + Ok(()) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum KeyBindingContextPredicate { + Identifier(SharedString), + Equal(SharedString, SharedString), + NotEqual(SharedString, SharedString), + Child( + Box, + Box, + ), + Not(Box), + And( + Box, + Box, + ), + Or( + Box, + Box, + ), +} + +impl KeyBindingContextPredicate { + pub fn parse(source: &str) -> Result { + let source = skip_whitespace(source); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if let Some(next) = rest.chars().next() { + Err(anyhow!("unexpected character {next:?}")) + } else { + Ok(predicate) + } + } + + pub fn eval(&self, contexts: &[KeyContext]) -> bool { + let Some(context) = contexts.last() else { + return false; + }; + match self { + Self::Identifier(name) => context.contains(name), + Self::Equal(left, right) => context + .get(left) + .map(|value| value == right) + .unwrap_or(false), + Self::NotEqual(left, right) => context + .get(left) + .map(|value| value != right) + .unwrap_or(true), + Self::Not(pred) => !pred.eval(contexts), + Self::Child(parent, child) => { + parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts) + } + Self::And(left, right) => left.eval(contexts) && right.eval(contexts), + Self::Or(left, right) => left.eval(contexts) || right.eval(contexts), + } + } + + fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> { + type Op = fn( + KeyBindingContextPredicate, + KeyBindingContextPredicate, + ) -> Result; + + let (mut predicate, rest) = Self::parse_primary(source)?; + source = rest; + + 'parse: loop { + for (operator, precedence, constructor) in [ + (">", PRECEDENCE_CHILD, Self::new_child as Op), + ("&&", PRECEDENCE_AND, Self::new_and as Op), + ("||", PRECEDENCE_OR, Self::new_or as Op), + ("==", PRECEDENCE_EQ, Self::new_eq as Op), + ("!=", PRECEDENCE_EQ, Self::new_neq as Op), + ] { + if source.starts_with(operator) && precedence >= min_precedence { + source = skip_whitespace(&source[operator.len()..]); + let (right, rest) = Self::parse_expr(source, precedence + 1)?; + predicate = constructor(predicate, right)?; + source = rest; + continue 'parse; + } + } + break; + } + + Ok((predicate, source)) + } + + fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> { + let next = source + .chars() + .next() + .ok_or_else(|| anyhow!("unexpected eof"))?; + match next { + '(' => { + source = skip_whitespace(&source[1..]); + let (predicate, rest) = Self::parse_expr(source, 0)?; + if rest.starts_with(')') { + source = skip_whitespace(&rest[1..]); + Ok((predicate, source)) + } else { + Err(anyhow!("expected a ')'")) + } + } + '!' => { + let source = skip_whitespace(&source[1..]); + let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?; + Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source)) + } + _ if is_identifier_char(next) => { + let len = source + .find(|c: char| !is_identifier_char(c)) + .unwrap_or(source.len()); + let (identifier, rest) = source.split_at(len); + source = skip_whitespace(rest); + Ok(( + KeyBindingContextPredicate::Identifier(identifier.to_string().into()), + source, + )) + } + _ => Err(anyhow!("unexpected character {next:?}")), + } + } + + fn new_or(self, other: Self) -> Result { + Ok(Self::Or(Box::new(self), Box::new(other))) + } + + fn new_and(self, other: Self) -> Result { + Ok(Self::And(Box::new(self), Box::new(other))) + } + + fn new_child(self, other: Self) -> Result { + Ok(Self::Child(Box::new(self), Box::new(other))) + } + + fn new_eq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::Equal(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } + + fn new_neq(self, other: Self) -> Result { + if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) { + Ok(Self::NotEqual(left, right)) + } else { + Err(anyhow!("operands must be identifiers")) + } + } +} + +const PRECEDENCE_CHILD: u32 = 1; +const PRECEDENCE_OR: u32 = 2; +const PRECEDENCE_AND: u32 = 3; +const PRECEDENCE_EQ: u32 = 4; +const PRECEDENCE_NOT: u32 = 5; + +fn is_identifier_char(c: char) -> bool { + c.is_alphanumeric() || c == '_' || c == '-' +} + +fn skip_whitespace(source: &str) -> &str { + let len = source + .find(|c: char| !c.is_whitespace()) + .unwrap_or(source.len()); + &source[len..] +} + +#[cfg(test)] +mod tests { + use super::*; + use crate as gpui; + use KeyBindingContextPredicate::*; + + #[test] + fn test_actions_definition() { + { + actions!(A, B, C, D, E, F, G); + } + + { + actions!( + A, + B, + C, + D, + E, + F, + G, // Don't wrap, test the trailing comma + ); + } + } + + #[test] + fn test_parse_context() { + let mut expected = KeyContext::default(); + expected.add("baz"); + expected.set("foo", "bar"); + assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); + assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected); + assert_eq!( + KeyContext::parse(" baz foo = bar baz").unwrap(), + expected + ); + assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected); + } + + #[test] + fn test_parse_identifiers() { + // Identifiers + assert_eq!( + KeyBindingContextPredicate::parse("abc12").unwrap(), + Identifier("abc12".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("_1a").unwrap(), + Identifier("_1a".into()) + ); + } + + #[test] + fn test_parse_negations() { + assert_eq!( + KeyBindingContextPredicate::parse("!abc").unwrap(), + Not(Box::new(Identifier("abc".into()))) + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ! ! abc").unwrap(), + Not(Box::new(Not(Box::new(Identifier("abc".into()))))) + ); + } + + #[test] + fn test_parse_equality_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a == b").unwrap(), + Equal("a".into(), "b".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c!=d").unwrap(), + NotEqual("c".into(), "d".into()) + ); + assert_eq!( + KeyBindingContextPredicate::parse("c == !d") + .unwrap_err() + .to_string(), + "operands must be identifiers" + ); + } + + #[test] + fn test_parse_boolean_operators() { + assert_eq!( + KeyBindingContextPredicate::parse("a || b").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a || !b && c").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(And( + Box::new(Not(Box::new(Identifier("b".into())))), + Box::new(Identifier("c".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(), + Or( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(And( + Box::new(Identifier("c".into())), + Box::new(Identifier("d".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(), + Or( + Box::new(And( + Box::new(Equal("a".into(), "b".into())), + Box::new(Identifier("c".into())) + )), + Box::new(And( + Box::new(Equal("d".into(), "e".into())), + Box::new(Identifier("f".into())) + )) + ) + ); + assert_eq!( + KeyBindingContextPredicate::parse("a && b && c && d").unwrap(), + And( + Box::new(And( + Box::new(And( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())) + )), + Box::new(Identifier("c".into())), + )), + Box::new(Identifier("d".into())) + ), + ); + } + + #[test] + fn test_parse_parenthesized_expressions() { + assert_eq!( + KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(), + And( + Box::new(Identifier("a".into())), + Box::new(Or( + Box::new(Equal("b".into(), "c".into())), + Box::new(NotEqual("d".into(), "e".into())), + )), + ), + ); + assert_eq!( + KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(), + Or( + Box::new(Identifier("a".into())), + Box::new(Identifier("b".into())), + ) + ); + } +} diff --git a/crates/gpui2/src/keymap/keymap.rs b/crates/gpui2/src/keymap/keymap.rs index eda493a4607837783e830069931d617692feeb9f..989ee7a8d5515182769eb10cfb5bacc3eeaf8501 100644 --- a/crates/gpui2/src/keymap/keymap.rs +++ b/crates/gpui2/src/keymap/keymap.rs @@ -1,4 +1,4 @@ -use crate::{DispatchContextPredicate, KeyBinding, Keystroke}; +use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke}; use collections::HashSet; use smallvec::SmallVec; use std::{any::TypeId, collections::HashMap}; @@ -11,7 +11,7 @@ pub struct Keymap { bindings: Vec, binding_indices_by_action_id: HashMap>, disabled_keystrokes: - HashMap, HashSet>>, + HashMap, HashSet>>, version: KeymapVersion, } diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index c2033a95953a46479c02bfef69f432f0c877b3ae..bab9c0f5757354b933849ca7b324061df4812cec 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -1,15 +1,15 @@ -use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke}; +use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke}; use parking_lot::Mutex; use smallvec::SmallVec; use std::sync::Arc; -pub struct KeyMatcher { +pub struct KeystrokeMatcher { pending_keystrokes: Vec, keymap: Arc>, keymap_version: KeymapVersion, } -impl KeyMatcher { +impl KeystrokeMatcher { pub fn new(keymap: Arc>) -> Self { let keymap_version = keymap.lock().version(); Self { @@ -44,7 +44,7 @@ impl KeyMatcher { pub fn match_keystroke( &mut self, keystroke: &Keystroke, - context_stack: &[&DispatchContext], + context_stack: &[KeyContext], ) -> KeyMatch { let keymap = self.keymap.lock(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. @@ -86,7 +86,7 @@ impl KeyMatcher { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[&DispatchContext], + contexts: &[KeyContext], ) -> Option> { self.keymap .lock() diff --git a/crates/gpui2/src/keymap/mod.rs b/crates/gpui2/src/keymap/mod.rs index 449b5427bf288dbf7647ed3ebd2451e15b10dd15..09e222c09552a80e7d187ec3303d1993d28979e8 100644 --- a/crates/gpui2/src/keymap/mod.rs +++ b/crates/gpui2/src/keymap/mod.rs @@ -1,7 +1,9 @@ mod binding; +mod context; mod keymap; mod matcher; pub use binding::*; +pub use context::*; pub use keymap::*; pub use matcher::*; diff --git a/crates/gpui2/src/prelude.rs b/crates/gpui2/src/prelude.rs new file mode 100644 index 0000000000000000000000000000000000000000..bc998fc1f45e144a22fd7f0103db44b27622b884 --- /dev/null +++ b/crates/gpui2/src/prelude.rs @@ -0,0 +1 @@ +pub use crate::{Context, ParentElement, Refineable}; diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index 00e1e55cd57d0fc96deb8d57be7f68f3c3b1f5bd..d12d84f43b424f83c5785d7dd0b33c776054bbb3 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -184,10 +184,6 @@ impl AnyView { .compute_layout(layout_id, available_space); (self.paint)(self, &mut rendered_element, cx); } - - pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) { - (self.initialize)(self, cx); - } } impl Component for AnyView { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 45d1ec69facd71c23fbe63f0b54383af3404cb00..fbc9cff1b568985ec05489283b961d92284570cf 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,15 +1,15 @@ use crate::{ - build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, + key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, - DevicePixels, DispatchContext, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, - FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, - IsZero, KeyListener, KeyMatch, KeyMatcher, Keystroke, LayoutId, Model, ModelContext, Modifiers, - MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, - PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, - PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, - SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, - TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, - WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, + EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, + InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, + Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, + PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, + RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, + Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, + WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; @@ -60,16 +60,7 @@ pub enum DispatchPhase { } type AnyObserver = Box bool + 'static>; -type AnyListener = Box; -type AnyKeyListener = Box< - dyn Fn( - &dyn Any, - &[&DispatchContext], - DispatchPhase, - &mut WindowContext, - ) -> Option> - + 'static, ->; +type AnyMouseListener = Box; type AnyFocusListener = Box; type AnyWindowFocusListener = Box bool + 'static>; @@ -97,20 +88,10 @@ impl FocusId { /// Obtains whether this handle contains the given handle in the most recently rendered frame. pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool { - let mut ancestor = Some(other); - while let Some(ancestor_id) = ancestor { - if *self == ancestor_id { - return true; - } else { - ancestor = cx - .window - .current_frame - .focus_parents_by_child - .get(&ancestor_id) - .copied(); - } - } - false + cx.window + .current_frame + .dispatch_tree + .focus_contains(*self, other) } } @@ -227,20 +208,31 @@ pub struct Window { pub(crate) focus: Option, } -#[derive(Default)] +// #[derive(Default)] pub(crate) struct Frame { element_states: HashMap, - key_matchers: HashMap, - mouse_listeners: HashMap>, + mouse_listeners: HashMap>, + pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, - pub(crate) key_dispatch_stack: Vec, - freeze_key_dispatch_stack: bool, - focus_parents_by_child: HashMap, pub(crate) scene_builder: SceneBuilder, z_index_stack: StackingOrder, content_mask_stack: Vec>, element_offset_stack: Vec>, - focus_stack: Vec, +} + +impl Frame { + pub fn new(dispatch_tree: DispatchTree) -> Self { + Frame { + element_states: HashMap::default(), + mouse_listeners: HashMap::default(), + dispatch_tree, + focus_listeners: Vec::new(), + scene_builder: SceneBuilder::default(), + z_index_stack: StackingOrder::default(), + content_mask_stack: Vec::new(), + element_offset_stack: Vec::new(), + } + } } impl Window { @@ -309,8 +301,8 @@ impl Window { layout_engine: TaffyLayoutEngine::new(), root_view: None, element_id_stack: GlobalElementId::default(), - previous_frame: Frame::default(), - current_frame: Frame::default(), + previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())), + current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_listeners: SubscriberSet::new(), default_prevented: true, @@ -328,18 +320,6 @@ impl Window { } } -/// When constructing the element tree, we maintain a stack of key dispatch frames until we -/// find the focused element. We interleave key listeners with dispatch contexts so we can use the -/// contexts when matching key events against the keymap. A key listener can be either an action -/// handler or a [KeyDown] / [KeyUp] event listener. -pub(crate) enum KeyDispatchStackFrame { - Listener { - event_type: TypeId, - listener: AnyKeyListener, - }, - Context(DispatchContext), -} - /// Indicates which region of the window is visible. Content falling outside of this mask will not be /// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type /// to leave room to support more complex shapes in the future. @@ -407,21 +387,16 @@ impl<'a> WindowContext<'a> { /// Move focus to the element associated with the given `FocusHandle`. pub fn focus(&mut self, handle: &FocusHandle) { - if self.window.focus == Some(handle.id) { - return; - } + let focus_id = handle.id; if self.window.last_blur.is_none() { self.window.last_blur = Some(self.window.focus); } - self.window.focus = Some(handle.id); - - // self.window.current_frame.key_dispatch_stack.clear() - // self.window.root_view.initialize() + self.window.focus = Some(focus_id); self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, - focused: Some(handle.id), + focused: Some(focus_id), }); self.notify(); } @@ -441,11 +416,18 @@ impl<'a> WindowContext<'a> { } pub fn dispatch_action(&mut self, action: Box) { - self.defer(|cx| { - cx.app.propagate_event = true; - let stack = cx.dispatch_stack(); - cx.dispatch_action_internal(action, &stack[..]) - }) + if let Some(focus_handle) = self.focused() { + self.defer(move |cx| { + if let Some(node_id) = cx + .window + .current_frame + .dispatch_tree + .focusable_node_id(focus_handle.id) + { + cx.dispatch_action_on_node(node_id, action); + } + }) + } } /// Schedules the given function to be run at the end of the current effect cycle, allowing entities @@ -731,6 +713,43 @@ impl<'a> WindowContext<'a> { )) } + /// Register a key event listener on the window for the current frame. The type of event + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using event handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_key_event( + &mut self, + handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, + ) { + self.window + .current_frame + .dispatch_tree + .on_key_event(Rc::new(move |event, phase, cx| { + if let Some(event) = event.downcast_ref::() { + handler(event, phase, cx) + } + })); + } + + /// Register an action listener on the window for the current frame. The type of action + /// is determined by the first parameter of the given listener. When the next frame is rendered + /// the listener will be cleared. + /// + /// This is a fairly low-level method, so prefer using action handlers on elements unless you have + /// a specific need to register a global listener. + pub fn on_action( + &mut self, + action_type: TypeId, + handler: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static, + ) { + self.window.current_frame.dispatch_tree.on_action( + action_type, + Rc::new(move |action, phase, cx| handler(action, phase, cx)), + ); + } + /// The position of the mouse relative to the window. pub fn mouse_position(&self) -> Point { self.window.mouse_position @@ -1079,26 +1098,6 @@ impl<'a> WindowContext<'a> { self.window.dirty = false; } - pub(crate) fn dispatch_stack(&mut self) -> Vec { - let root_view = self.window.root_view.take().unwrap(); - let window = &mut *self.window; - let mut spare_frame = Frame::default(); - mem::swap(&mut spare_frame, &mut window.previous_frame); - - self.start_frame(); - - root_view.draw_dispatch_stack(self); - - let window = &mut *self.window; - // restore the old values of current and previous frame, - // putting the new frame into spare_frame. - mem::swap(&mut window.current_frame, &mut window.previous_frame); - mem::swap(&mut spare_frame, &mut window.previous_frame); - self.window.root_view = Some(root_view); - - spare_frame.key_dispatch_stack - } - /// Rotate the current frame and the previous frame, then clear the current frame. /// We repopulate all state in the current frame during each paint. fn start_frame(&mut self) { @@ -1110,12 +1109,9 @@ impl<'a> WindowContext<'a> { mem::swap(&mut window.previous_frame, &mut window.current_frame); let frame = &mut window.current_frame; frame.element_states.clear(); - frame.key_matchers.clear(); frame.mouse_listeners.values_mut().for_each(Vec::clear); frame.focus_listeners.clear(); - frame.key_dispatch_stack.clear(); - frame.focus_parents_by_child.clear(); - frame.freeze_key_dispatch_stack = false; + frame.dispatch_tree.clear(); } /// Dispatch a mouse or keyboard event on the window. @@ -1177,146 +1173,172 @@ impl<'a> WindowContext<'a> { }; if let Some(any_mouse_event) = event.mouse_event() { - if let Some(mut handlers) = self - .window - .current_frame - .mouse_listeners - .remove(&any_mouse_event.type_id()) - { - // Because handlers may add other handlers, we sort every time. - handlers.sort_by(|(a, _), (b, _)| a.cmp(b)); + self.dispatch_mouse_event(any_mouse_event); + } else if let Some(any_key_event) = event.keyboard_event() { + self.dispatch_key_event(any_key_event); + } - // Capture phase, events bubble from back to front. Handlers for this phase are used for - // special purposes, such as detecting events outside of a given Bounds. - for (_, handler) in &mut handlers { - handler(any_mouse_event, DispatchPhase::Capture, self); - if !self.app.propagate_event { - break; - } + !self.app.propagate_event + } + + fn dispatch_mouse_event(&mut self, event: &dyn Any) { + if let Some(mut handlers) = self + .window + .current_frame + .mouse_listeners + .remove(&event.type_id()) + { + // Because handlers may add other handlers, we sort every time. + handlers.sort_by(|(a, _), (b, _)| a.cmp(b)); + + // Capture phase, events bubble from back to front. Handlers for this phase are used for + // special purposes, such as detecting events outside of a given Bounds. + for (_, handler) in &mut handlers { + handler(event, DispatchPhase::Capture, self); + if !self.app.propagate_event { + break; } + } - // Bubble phase, where most normal handlers do their work. - if self.app.propagate_event { - for (_, handler) in handlers.iter_mut().rev() { - handler(any_mouse_event, DispatchPhase::Bubble, self); - if !self.app.propagate_event { - break; - } + // Bubble phase, where most normal handlers do their work. + if self.app.propagate_event { + for (_, handler) in handlers.iter_mut().rev() { + handler(event, DispatchPhase::Bubble, self); + if !self.app.propagate_event { + break; } } + } - if self.app.propagate_event - && any_mouse_event.downcast_ref::().is_some() - { - self.active_drag = None; - } + if self.app.propagate_event && event.downcast_ref::().is_some() { + self.active_drag = None; + } - // Just in case any handlers added new handlers, which is weird, but possible. - handlers.extend( - self.window - .current_frame - .mouse_listeners - .get_mut(&any_mouse_event.type_id()) - .into_iter() - .flat_map(|handlers| handlers.drain(..)), - ); + // Just in case any handlers added new handlers, which is weird, but possible. + handlers.extend( self.window .current_frame .mouse_listeners - .insert(any_mouse_event.type_id(), handlers); - } - } else if let Some(any_key_event) = event.keyboard_event() { - let key_dispatch_stack = mem::take(&mut self.window.current_frame.key_dispatch_stack); - let key_event_type = any_key_event.type_id(); - let mut context_stack = SmallVec::<[&DispatchContext; 16]>::new(); - - for (ix, frame) in key_dispatch_stack.iter().enumerate() { - match frame { - KeyDispatchStackFrame::Listener { - event_type, - listener, - } => { - if key_event_type == *event_type { - if let Some(action) = listener( - any_key_event, - &context_stack, - DispatchPhase::Capture, - self, - ) { - self.dispatch_action_internal(action, &key_dispatch_stack[..ix]); - } - if !self.app.propagate_event { - break; - } - } - } - KeyDispatchStackFrame::Context(context) => { - context_stack.push(&context); + .get_mut(&event.type_id()) + .into_iter() + .flat_map(|handlers| handlers.drain(..)), + ); + self.window + .current_frame + .mouse_listeners + .insert(event.type_id(), handlers); + } + } + + fn dispatch_key_event(&mut self, event: &dyn Any) { + if let Some(node_id) = self.window.focus.and_then(|focus_id| { + self.window + .current_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) { + let dispatch_path = self + .window + .current_frame + .dispatch_tree + .dispatch_path(node_id); + + // Capture phase + let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); + self.propagate_event = true; + + for node_id in &dispatch_path { + let node = self.window.current_frame.dispatch_tree.node(*node_id); + + if !node.context.is_empty() { + context_stack.push(node.context.clone()); + } + + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Capture, self); + if !self.propagate_event { + return; } } } - if self.app.propagate_event { - for (ix, frame) in key_dispatch_stack.iter().enumerate().rev() { - match frame { - KeyDispatchStackFrame::Listener { - event_type, - listener, - } => { - if key_event_type == *event_type { - if let Some(action) = listener( - any_key_event, - &context_stack, - DispatchPhase::Bubble, - self, - ) { - self.dispatch_action_internal( - action, - &key_dispatch_stack[..ix], - ); - } - - if !self.app.propagate_event { - break; - } + // Bubble phase + for node_id in dispatch_path.iter().rev() { + // Handle low level key events + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Bubble, self); + if !self.propagate_event { + return; + } + } + + // Match keystrokes + let node = self.window.current_frame.dispatch_tree.node(*node_id); + if !node.context.is_empty() { + if let Some(key_down_event) = event.downcast_ref::() { + if let Some(action) = self + .window + .current_frame + .dispatch_tree + .dispatch_key(&key_down_event.keystroke, &context_stack) + { + self.dispatch_action_on_node(*node_id, action); + if !self.propagate_event { + return; } } - KeyDispatchStackFrame::Context(_) => { - context_stack.pop(); - } } + + context_stack.pop(); } } - - drop(context_stack); - self.window.current_frame.key_dispatch_stack = key_dispatch_stack; } - - !self.app.propagate_event } - /// Attempt to map a keystroke to an action based on the keymap. - pub fn match_keystroke( - &mut self, - element_id: &GlobalElementId, - keystroke: &Keystroke, - context_stack: &[&DispatchContext], - ) -> KeyMatch { - let key_match = self + fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box) { + let dispatch_path = self .window .current_frame - .key_matchers - .get_mut(element_id) - .unwrap() - .match_keystroke(keystroke, context_stack); - - if key_match.is_some() { - for matcher in self.window.current_frame.key_matchers.values_mut() { - matcher.clear_pending(); + .dispatch_tree + .dispatch_path(node_id); + + // Capture phase + for node_id in &dispatch_path { + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for DispatchActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + listener(any_action, DispatchPhase::Capture, self); + if !self.propagate_event { + return; + } + } } } - key_match + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for DispatchActionListener { + action_type, + listener, + } in node.action_listeners.clone() + { + let any_action = action.as_any(); + if action_type == any_action.type_id() { + self.propagate_event = false; // Actions stop propagation by default during the bubble phase + listener(any_action, DispatchPhase::Bubble, self); + if !self.propagate_event { + return; + } + } + } + } } /// Register the given handler to be invoked whenever the global of the given type @@ -1345,106 +1367,22 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - pub fn available_actions(&self) -> impl Iterator> + '_ { - let key_dispatch_stack = &self.window.previous_frame.key_dispatch_stack; - key_dispatch_stack.iter().filter_map(|frame| { - match frame { - // todo!factor out a KeyDispatchStackFrame::Action - KeyDispatchStackFrame::Listener { - event_type, - listener: _, - } => { - match build_action_from_type(event_type) { - Ok(action) => Some(action), - Err(err) => { - dbg!(err); - None - } // we'll hit his if TypeId == KeyDown - } - } - KeyDispatchStackFrame::Context(_) => None, - } - }) - } - - pub(crate) fn dispatch_action_internal( - &mut self, - action: Box, - dispatch_stack: &[KeyDispatchStackFrame], - ) { - let action_type = action.as_any().type_id(); - - if let Some(mut global_listeners) = self.app.global_action_listeners.remove(&action_type) { - for listener in &global_listeners { - listener(action.as_ref(), DispatchPhase::Capture, self); - if !self.app.propagate_event { - break; - } - } - global_listeners.extend( - self.global_action_listeners - .remove(&action_type) - .unwrap_or_default(), - ); - self.global_action_listeners - .insert(action_type, global_listeners); - } - - if self.app.propagate_event { - for stack_frame in dispatch_stack { - if let KeyDispatchStackFrame::Listener { - event_type, - listener, - } = stack_frame - { - if action_type == *event_type { - listener(action.as_any(), &[], DispatchPhase::Capture, self); - if !self.app.propagate_event { - break; - } - } - } - } - } - - if self.app.propagate_event { - for stack_frame in dispatch_stack.iter().rev() { - if let KeyDispatchStackFrame::Listener { - event_type, - listener, - } = stack_frame - { - if action_type == *event_type { - self.app.propagate_event = false; - listener(action.as_any(), &[], DispatchPhase::Bubble, self); - if !self.app.propagate_event { - break; - } - } - } - } + pub fn available_actions(&self) -> Vec> { + if let Some(focus_id) = self.window.focus { + self.window + .current_frame + .dispatch_tree + .available_actions(focus_id) + } else { + Vec::new() } + } - if self.app.propagate_event { - if let Some(mut global_listeners) = - self.app.global_action_listeners.remove(&action_type) - { - for listener in global_listeners.iter().rev() { - self.app.propagate_event = false; - listener(action.as_ref(), DispatchPhase::Bubble, self); - if !self.app.propagate_event { - break; - } - } - global_listeners.extend( - self.global_action_listeners - .remove(&action_type) - .unwrap_or_default(), - ); - self.global_action_listeners - .insert(action_type, global_listeners); - } - } + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.window + .current_frame + .dispatch_tree + .bindings_for_action(action) } } @@ -1631,22 +1569,9 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { id: impl Into, f: impl FnOnce(GlobalElementId, &mut Self) -> R, ) -> R { - let keymap = self.app_mut().keymap.clone(); let window = self.window_mut(); window.element_id_stack.push(id.into()); let global_id = window.element_id_stack.clone(); - - if window.current_frame.key_matchers.get(&global_id).is_none() { - window.current_frame.key_matchers.insert( - global_id.clone(), - window - .previous_frame - .key_matchers - .remove(&global_id) - .unwrap_or_else(|| KeyMatcher::new(keymap)), - ); - } - let result = f(global_id, self); let window: &mut Window = self.borrow_mut(); window.element_id_stack.pop(); @@ -2134,94 +2059,28 @@ impl<'a, V: 'static> ViewContext<'a, V> { })); } - pub fn with_key_listeners( - &mut self, - key_listeners: impl IntoIterator)>, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - let old_stack_len = self.window.current_frame.key_dispatch_stack.len(); - if !self.window.current_frame.freeze_key_dispatch_stack { - for (event_type, listener) in key_listeners { - let handle = self.view().downgrade(); - let listener = Box::new( - move |event: &dyn Any, - context_stack: &[&DispatchContext], - phase: DispatchPhase, - cx: &mut WindowContext<'_>| { - handle - .update(cx, |view, cx| { - listener(view, event, context_stack, phase, cx) - }) - .log_err() - .flatten() - }, - ); - self.window.current_frame.key_dispatch_stack.push( - KeyDispatchStackFrame::Listener { - event_type, - listener, - }, - ); - } - } - - let result = f(self); - - if !self.window.current_frame.freeze_key_dispatch_stack { - self.window - .current_frame - .key_dispatch_stack - .truncate(old_stack_len); - } - - result - } - - pub fn with_key_dispatch_context( + pub fn with_key_dispatch( &mut self, - context: DispatchContext, - f: impl FnOnce(&mut Self) -> R, + context: KeyContext, + focus_handle: Option, + f: impl FnOnce(Option, &mut Self) -> R, ) -> R { - if context.is_empty() { - return f(self); - } - - if !self.window.current_frame.freeze_key_dispatch_stack { - self.window - .current_frame - .key_dispatch_stack - .push(KeyDispatchStackFrame::Context(context)); - } - - let result = f(self); + let window = &mut self.window; - if !self.window.previous_frame.freeze_key_dispatch_stack { - self.window.previous_frame.key_dispatch_stack.pop(); - } - - result - } - - pub fn with_focus( - &mut self, - focus_handle: FocusHandle, - f: impl FnOnce(&mut Self) -> R, - ) -> R { - if let Some(parent_focus_id) = self.window.current_frame.focus_stack.last().copied() { - self.window + window + .current_frame + .dispatch_tree + .push_node(context, &mut window.previous_frame.dispatch_tree); + if let Some(focus_handle) = focus_handle.as_ref() { + window .current_frame - .focus_parents_by_child - .insert(focus_handle.id, parent_focus_id); + .dispatch_tree + .make_focusable(focus_handle.id); } - self.window.current_frame.focus_stack.push(focus_handle.id); + let result = f(focus_handle, self); - if Some(focus_handle.id) == self.window.focus { - self.window.current_frame.freeze_key_dispatch_stack = true; - } + self.window.current_frame.dispatch_tree.pop_node(); - let result = f(self); - - self.window.current_frame.focus_stack.pop(); result } @@ -2275,6 +2134,32 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + pub fn on_key_event( + &mut self, + handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, + ) { + let handle = self.view(); + self.window_cx.on_key_event(move |event, phase, cx| { + handle.update(cx, |view, cx| { + handler(view, event, phase, cx); + }) + }); + } + + pub fn on_action( + &mut self, + action_type: TypeId, + handler: impl Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext) + 'static, + ) { + let handle = self.view(); + self.window_cx + .on_action(action_type, move |action, phase, cx| { + handle.update(cx, |view, cx| { + handler(view, action, phase, cx); + }) + }); + } + /// Set an input handler, such as [ElementInputHandler], which interfaces with the /// platform to receive textual input with proper integration with concerns such /// as IME interactions. diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index ac1c5f89ea07e058d39ed07173437f3c59cf3c56..e1979f1b13455eedd07f9d9ca07609642a3a0b83 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,11 +1,10 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity, - StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, - WindowContext, + div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled, Task, + UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; -use std::cmp; -use ui::{prelude::*, v_stack, Divider}; +use std::{cmp, sync::Arc}; +use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; pub struct Picker { pub delegate: D, @@ -21,7 +20,7 @@ pub trait PickerDelegate: Sized + 'static { fn selected_index(&self) -> usize; fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); - // fn placeholder_text(&self) -> Arc; + fn placeholder_text(&self) -> Arc; fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()>; fn confirm(&mut self, secondary: bool, cx: &mut ViewContext>); @@ -37,7 +36,11 @@ pub trait PickerDelegate: Sized + 'static { impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { - let editor = cx.build_view(|cx| Editor::single_line(cx)); + let editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text(delegate.placeholder_text(), cx); + editor + }); cx.subscribe(&editor, Self::on_input_editor_event).detach(); Self { delegate, @@ -136,13 +139,11 @@ impl Picker { } impl Render for Picker { - type Element = Div, FocusEnabled>; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .context("picker") - .id("picker-container") - .focusable() .size_full() .elevation_2(cx) .on_action(Self::select_next) @@ -159,23 +160,35 @@ impl Render for Picker { .child(div().px_1().py_0p5().child(self.editor.clone())), ) .child(Divider::horizontal()) - .child( - v_stack() - .p_1() - .grow() - .child( - uniform_list("candidates", self.delegate.match_count(), { - move |this: &mut Self, visible_range, cx| { - let selected_ix = this.delegate.selected_index(); - visible_range - .map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) - .collect() - } - }) - .track_scroll(self.scroll_handle.clone()), - ) - .max_h_72() - .overflow_hidden(), - ) + .when(self.delegate.match_count() > 0, |el| { + el.child( + v_stack() + .p_1() + .grow() + .child( + uniform_list("candidates", self.delegate.match_count(), { + move |this: &mut Self, visible_range, cx| { + let selected_ix = this.delegate.selected_index(); + visible_range + .map(|ix| { + this.delegate.render_match(ix, ix == selected_ix, cx) + }) + .collect() + } + }) + .track_scroll(self.scroll_handle.clone()), + ) + .max_h_72() + .overflow_hidden(), + ) + }) + .when(self.delegate.match_count() == 0, |el| { + el.child( + v_stack() + .p_1() + .grow() + .child(Label::new("No matches").color(LabelColor::Muted)), + ) + }) } } diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 984ee421db3b357545de400547bd5057365610da..bba798d9fe26403257d8a42fd2842ff8a89526d9 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,12 +1,16 @@ use gpui::{ - actions, div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, - StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, WindowContext, + actions, div, Div, FocusHandle, Focusable, FocusableKeyDispatch, KeyBinding, ParentElement, + Render, StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, + WindowContext, }; use theme2::ActiveTheme; actions!(ActionA, ActionB, ActionC); -pub struct FocusStory {} +pub struct FocusStory { + child_1_focus: FocusHandle, + child_2_focus: FocusHandle, +} impl FocusStory { pub fn view(cx: &mut WindowContext) -> View { @@ -16,12 +20,15 @@ impl FocusStory { KeyBinding::new("cmd-c", ActionC, None), ]); - cx.build_view(move |cx| Self {}) + cx.build_view(move |cx| Self { + child_1_focus: cx.focus_handle(), + child_2_focus: cx.focus_handle(), + }) } } impl Render for FocusStory { - type Element = Div, FocusEnabled>; + type Element = Div, FocusableKeyDispatch>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); @@ -31,18 +38,16 @@ impl Render for FocusStory { let color_4 = theme.status().conflict; let color_5 = theme.status().ignored; let color_6 = theme.status().renamed; - let child_1 = cx.focus_handle(); - let child_2 = cx.focus_handle(); div() .id("parent") .focusable() .context("parent") .on_action(|_, action: &ActionA, cx| { - println!("Action A dispatched on parent during"); + println!("Action A dispatched on parent"); }) .on_action(|_, action: &ActionB, cx| { - println!("Action B dispatched on parent during"); + println!("Action B dispatched on parent"); }) .on_focus(|_, _, _| println!("Parent focused")) .on_blur(|_, _, _| println!("Parent blurred")) @@ -56,7 +61,7 @@ impl Render for FocusStory { .focus_in(|style| style.bg(color_3)) .child( div() - .track_focus(&child_1) + .track_focus(&self.child_1_focus) .context("child-1") .on_action(|_, action: &ActionB, cx| { println!("Action B dispatched on child 1 during"); @@ -76,10 +81,10 @@ impl Render for FocusStory { ) .child( div() - .track_focus(&child_2) + .track_focus(&self.child_2_focus) .context("child-2") .on_action(|_, action: &ActionC, cx| { - println!("Action C dispatched on child 2 during"); + println!("Action C dispatched on child 2"); }) .w_full() .h_6() diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 82a010e6b30e820681b903232b74c35cad6b8584..067c190575acebad345479b88fe6cf164c19ef81 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -44,6 +44,10 @@ impl PickerDelegate for Delegate { self.candidates.len() } + fn placeholder_text(&self) -> Arc { + "Test".into() + } + fn render_match( &self, ix: usize, diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index f0ba124162d546bcdbdc1fce12e986364f950dad..c4c1d75eac26b245ede5b79b6042e33cbeec67f7 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -48,7 +48,7 @@ fn main() { let args = Args::parse(); let story_selector = args.story.clone(); - let theme_name = args.theme.unwrap_or("Zed Pro Moonlight".to_string()); + let theme_name = args.theme.unwrap_or("One Dark".to_string()); let asset_source = Arc::new(Assets); gpui::App::production(asset_source).run(move |cx| { diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 6cfda37a2a632a2d7228152409c1deb1c00c2aa5..91efecbfb310103deeafb683becf31b9b7732c42 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -1,261 +1,15 @@ -use gpui::{hsla, Hsla, Rgba}; +use gpui::{Hsla, Rgba}; -use crate::colors::{StatusColors, SystemColors, ThemeColors}; use crate::scale::{ColorScaleSet, ColorScales}; -use crate::syntax::SyntaxTheme; -use crate::{ColorScale, PlayerColor, PlayerColors}; +use crate::ColorScale; +use crate::{SystemColors, ThemeColors}; -impl Default for PlayerColors { - fn default() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().dark().step_9(), - background: blue().dark().step_5(), - selection: blue().dark().step_3(), - }, - PlayerColor { - cursor: orange().dark().step_9(), - background: orange().dark().step_5(), - selection: orange().dark().step_3(), - }, - PlayerColor { - cursor: pink().dark().step_9(), - background: pink().dark().step_5(), - selection: pink().dark().step_3(), - }, - PlayerColor { - cursor: lime().dark().step_9(), - background: lime().dark().step_5(), - selection: lime().dark().step_3(), - }, - PlayerColor { - cursor: purple().dark().step_9(), - background: purple().dark().step_5(), - selection: purple().dark().step_3(), - }, - PlayerColor { - cursor: amber().dark().step_9(), - background: amber().dark().step_5(), - selection: amber().dark().step_3(), - }, - PlayerColor { - cursor: jade().dark().step_9(), - background: jade().dark().step_5(), - selection: jade().dark().step_3(), - }, - PlayerColor { - cursor: red().dark().step_9(), - background: red().dark().step_5(), - selection: red().dark().step_3(), - }, - ]) - } -} - -impl PlayerColors { - pub fn default_light() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().light().step_9(), - background: blue().light().step_4(), - selection: blue().light().step_3(), - }, - PlayerColor { - cursor: orange().light().step_9(), - background: orange().light().step_4(), - selection: orange().light().step_3(), - }, - PlayerColor { - cursor: pink().light().step_9(), - background: pink().light().step_4(), - selection: pink().light().step_3(), - }, - PlayerColor { - cursor: lime().light().step_9(), - background: lime().light().step_4(), - selection: lime().light().step_3(), - }, - PlayerColor { - cursor: purple().light().step_9(), - background: purple().light().step_4(), - selection: purple().light().step_3(), - }, - PlayerColor { - cursor: amber().light().step_9(), - background: amber().light().step_4(), - selection: amber().light().step_3(), - }, - PlayerColor { - cursor: jade().light().step_9(), - background: jade().light().step_4(), - selection: jade().light().step_3(), - }, - PlayerColor { - cursor: red().light().step_9(), - background: red().light().step_4(), - selection: red().light().step_3(), - }, - ]) - } -} - -fn neutral() -> ColorScaleSet { +pub(crate) fn neutral() -> ColorScaleSet { slate() } -impl Default for SystemColors { - fn default() -> Self { - Self { - transparent: hsla(0.0, 0.0, 0.0, 0.0), - mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), - mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), - mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), - } - } -} - -impl Default for StatusColors { - fn default() -> Self { - Self { - conflict: red().dark().step_9(), - created: grass().dark().step_9(), - deleted: red().dark().step_9(), - error: red().dark().step_9(), - hidden: neutral().dark().step_9(), - ignored: neutral().dark().step_9(), - info: blue().dark().step_9(), - modified: yellow().dark().step_9(), - renamed: blue().dark().step_9(), - success: grass().dark().step_9(), - warning: yellow().dark().step_9(), - } - } -} - -impl SyntaxTheme { - pub fn default_light() -> Self { - Self { - highlights: vec![ - ("attribute".into(), cyan().light().step_11().into()), - ("boolean".into(), tomato().light().step_11().into()), - ("comment".into(), neutral().light().step_11().into()), - ("comment.doc".into(), iris().light().step_12().into()), - ("constant".into(), red().light().step_9().into()), - ("constructor".into(), red().light().step_9().into()), - ("embedded".into(), red().light().step_9().into()), - ("emphasis".into(), red().light().step_9().into()), - ("emphasis.strong".into(), red().light().step_9().into()), - ("enum".into(), red().light().step_9().into()), - ("function".into(), red().light().step_9().into()), - ("hint".into(), red().light().step_9().into()), - ("keyword".into(), orange().light().step_11().into()), - ("label".into(), red().light().step_9().into()), - ("link_text".into(), red().light().step_9().into()), - ("link_uri".into(), red().light().step_9().into()), - ("number".into(), red().light().step_9().into()), - ("operator".into(), red().light().step_9().into()), - ("predictive".into(), red().light().step_9().into()), - ("preproc".into(), red().light().step_9().into()), - ("primary".into(), red().light().step_9().into()), - ("property".into(), red().light().step_9().into()), - ("punctuation".into(), neutral().light().step_11().into()), - ( - "punctuation.bracket".into(), - neutral().light().step_11().into(), - ), - ( - "punctuation.delimiter".into(), - neutral().light().step_11().into(), - ), - ( - "punctuation.list_marker".into(), - blue().light().step_11().into(), - ), - ("punctuation.special".into(), red().light().step_9().into()), - ("string".into(), jade().light().step_11().into()), - ("string.escape".into(), red().light().step_9().into()), - ("string.regex".into(), tomato().light().step_11().into()), - ("string.special".into(), red().light().step_9().into()), - ( - "string.special.symbol".into(), - red().light().step_9().into(), - ), - ("tag".into(), red().light().step_9().into()), - ("text.literal".into(), red().light().step_9().into()), - ("title".into(), red().light().step_9().into()), - ("type".into(), red().light().step_9().into()), - ("variable".into(), red().light().step_9().into()), - ("variable.special".into(), red().light().step_9().into()), - ("variant".into(), red().light().step_9().into()), - ], - inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style") - suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style") - } - } - - pub fn default_dark() -> Self { - Self { - highlights: vec![ - ("attribute".into(), tomato().dark().step_11().into()), - ("boolean".into(), tomato().dark().step_11().into()), - ("comment".into(), neutral().dark().step_11().into()), - ("comment.doc".into(), iris().dark().step_12().into()), - ("constant".into(), orange().dark().step_11().into()), - ("constructor".into(), gold().dark().step_11().into()), - ("embedded".into(), red().dark().step_11().into()), - ("emphasis".into(), red().dark().step_11().into()), - ("emphasis.strong".into(), red().dark().step_11().into()), - ("enum".into(), yellow().dark().step_11().into()), - ("function".into(), blue().dark().step_11().into()), - ("hint".into(), indigo().dark().step_11().into()), - ("keyword".into(), plum().dark().step_11().into()), - ("label".into(), red().dark().step_11().into()), - ("link_text".into(), red().dark().step_11().into()), - ("link_uri".into(), red().dark().step_11().into()), - ("number".into(), red().dark().step_11().into()), - ("operator".into(), red().dark().step_11().into()), - ("predictive".into(), red().dark().step_11().into()), - ("preproc".into(), red().dark().step_11().into()), - ("primary".into(), red().dark().step_11().into()), - ("property".into(), red().dark().step_11().into()), - ("punctuation".into(), neutral().dark().step_11().into()), - ( - "punctuation.bracket".into(), - neutral().dark().step_11().into(), - ), - ( - "punctuation.delimiter".into(), - neutral().dark().step_11().into(), - ), - ( - "punctuation.list_marker".into(), - blue().dark().step_11().into(), - ), - ("punctuation.special".into(), red().dark().step_11().into()), - ("string".into(), lime().dark().step_11().into()), - ("string.escape".into(), orange().dark().step_11().into()), - ("string.regex".into(), tomato().dark().step_11().into()), - ("string.special".into(), red().dark().step_11().into()), - ( - "string.special.symbol".into(), - red().dark().step_11().into(), - ), - ("tag".into(), red().dark().step_11().into()), - ("text.literal".into(), purple().dark().step_11().into()), - ("title".into(), sky().dark().step_11().into()), - ("type".into(), mint().dark().step_11().into()), - ("variable".into(), red().dark().step_11().into()), - ("variable.special".into(), red().dark().step_11().into()), - ("variant".into(), red().dark().step_11().into()), - ], - inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style") - suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style") - } - } -} - impl ThemeColors { - pub fn default_light() -> Self { + pub fn light() -> Self { let system = SystemColors::default(); Self { @@ -327,7 +81,7 @@ impl ThemeColors { } } - pub fn default_dark() -> Self { + pub fn dark() -> Self { let system = SystemColors::default(); Self { @@ -470,7 +224,7 @@ pub fn default_color_scales() -> ColorScales { } } -fn gray() -> ColorScaleSet { +pub(crate) fn gray() -> ColorScaleSet { StaticColorScaleSet { scale: "Gray", light: [ @@ -534,7 +288,7 @@ fn gray() -> ColorScaleSet { .unwrap() } -fn mauve() -> ColorScaleSet { +pub(crate) fn mauve() -> ColorScaleSet { StaticColorScaleSet { scale: "Mauve", light: [ @@ -598,7 +352,7 @@ fn mauve() -> ColorScaleSet { .unwrap() } -fn slate() -> ColorScaleSet { +pub(crate) fn slate() -> ColorScaleSet { StaticColorScaleSet { scale: "Slate", light: [ @@ -662,7 +416,7 @@ fn slate() -> ColorScaleSet { .unwrap() } -fn sage() -> ColorScaleSet { +pub(crate) fn sage() -> ColorScaleSet { StaticColorScaleSet { scale: "Sage", light: [ @@ -726,7 +480,7 @@ fn sage() -> ColorScaleSet { .unwrap() } -fn olive() -> ColorScaleSet { +pub(crate) fn olive() -> ColorScaleSet { StaticColorScaleSet { scale: "Olive", light: [ @@ -790,7 +544,7 @@ fn olive() -> ColorScaleSet { .unwrap() } -fn sand() -> ColorScaleSet { +pub(crate) fn sand() -> ColorScaleSet { StaticColorScaleSet { scale: "Sand", light: [ @@ -854,7 +608,7 @@ fn sand() -> ColorScaleSet { .unwrap() } -fn gold() -> ColorScaleSet { +pub(crate) fn gold() -> ColorScaleSet { StaticColorScaleSet { scale: "Gold", light: [ @@ -918,7 +672,7 @@ fn gold() -> ColorScaleSet { .unwrap() } -fn bronze() -> ColorScaleSet { +pub(crate) fn bronze() -> ColorScaleSet { StaticColorScaleSet { scale: "Bronze", light: [ @@ -982,7 +736,7 @@ fn bronze() -> ColorScaleSet { .unwrap() } -fn brown() -> ColorScaleSet { +pub(crate) fn brown() -> ColorScaleSet { StaticColorScaleSet { scale: "Brown", light: [ @@ -1046,7 +800,7 @@ fn brown() -> ColorScaleSet { .unwrap() } -fn yellow() -> ColorScaleSet { +pub(crate) fn yellow() -> ColorScaleSet { StaticColorScaleSet { scale: "Yellow", light: [ @@ -1110,7 +864,7 @@ fn yellow() -> ColorScaleSet { .unwrap() } -fn amber() -> ColorScaleSet { +pub(crate) fn amber() -> ColorScaleSet { StaticColorScaleSet { scale: "Amber", light: [ @@ -1174,7 +928,7 @@ fn amber() -> ColorScaleSet { .unwrap() } -fn orange() -> ColorScaleSet { +pub(crate) fn orange() -> ColorScaleSet { StaticColorScaleSet { scale: "Orange", light: [ @@ -1238,7 +992,7 @@ fn orange() -> ColorScaleSet { .unwrap() } -fn tomato() -> ColorScaleSet { +pub(crate) fn tomato() -> ColorScaleSet { StaticColorScaleSet { scale: "Tomato", light: [ @@ -1302,7 +1056,7 @@ fn tomato() -> ColorScaleSet { .unwrap() } -fn red() -> ColorScaleSet { +pub(crate) fn red() -> ColorScaleSet { StaticColorScaleSet { scale: "Red", light: [ @@ -1366,7 +1120,7 @@ fn red() -> ColorScaleSet { .unwrap() } -fn ruby() -> ColorScaleSet { +pub(crate) fn ruby() -> ColorScaleSet { StaticColorScaleSet { scale: "Ruby", light: [ @@ -1430,7 +1184,7 @@ fn ruby() -> ColorScaleSet { .unwrap() } -fn crimson() -> ColorScaleSet { +pub(crate) fn crimson() -> ColorScaleSet { StaticColorScaleSet { scale: "Crimson", light: [ @@ -1494,7 +1248,7 @@ fn crimson() -> ColorScaleSet { .unwrap() } -fn pink() -> ColorScaleSet { +pub(crate) fn pink() -> ColorScaleSet { StaticColorScaleSet { scale: "Pink", light: [ @@ -1558,7 +1312,7 @@ fn pink() -> ColorScaleSet { .unwrap() } -fn plum() -> ColorScaleSet { +pub(crate) fn plum() -> ColorScaleSet { StaticColorScaleSet { scale: "Plum", light: [ @@ -1622,7 +1376,7 @@ fn plum() -> ColorScaleSet { .unwrap() } -fn purple() -> ColorScaleSet { +pub(crate) fn purple() -> ColorScaleSet { StaticColorScaleSet { scale: "Purple", light: [ @@ -1686,7 +1440,7 @@ fn purple() -> ColorScaleSet { .unwrap() } -fn violet() -> ColorScaleSet { +pub(crate) fn violet() -> ColorScaleSet { StaticColorScaleSet { scale: "Violet", light: [ @@ -1750,7 +1504,7 @@ fn violet() -> ColorScaleSet { .unwrap() } -fn iris() -> ColorScaleSet { +pub(crate) fn iris() -> ColorScaleSet { StaticColorScaleSet { scale: "Iris", light: [ @@ -1814,7 +1568,7 @@ fn iris() -> ColorScaleSet { .unwrap() } -fn indigo() -> ColorScaleSet { +pub(crate) fn indigo() -> ColorScaleSet { StaticColorScaleSet { scale: "Indigo", light: [ @@ -1878,7 +1632,7 @@ fn indigo() -> ColorScaleSet { .unwrap() } -fn blue() -> ColorScaleSet { +pub(crate) fn blue() -> ColorScaleSet { StaticColorScaleSet { scale: "Blue", light: [ @@ -1942,7 +1696,7 @@ fn blue() -> ColorScaleSet { .unwrap() } -fn cyan() -> ColorScaleSet { +pub(crate) fn cyan() -> ColorScaleSet { StaticColorScaleSet { scale: "Cyan", light: [ @@ -2006,7 +1760,7 @@ fn cyan() -> ColorScaleSet { .unwrap() } -fn teal() -> ColorScaleSet { +pub(crate) fn teal() -> ColorScaleSet { StaticColorScaleSet { scale: "Teal", light: [ @@ -2070,7 +1824,7 @@ fn teal() -> ColorScaleSet { .unwrap() } -fn jade() -> ColorScaleSet { +pub(crate) fn jade() -> ColorScaleSet { StaticColorScaleSet { scale: "Jade", light: [ @@ -2134,7 +1888,7 @@ fn jade() -> ColorScaleSet { .unwrap() } -fn green() -> ColorScaleSet { +pub(crate) fn green() -> ColorScaleSet { StaticColorScaleSet { scale: "Green", light: [ @@ -2198,7 +1952,7 @@ fn green() -> ColorScaleSet { .unwrap() } -fn grass() -> ColorScaleSet { +pub(crate) fn grass() -> ColorScaleSet { StaticColorScaleSet { scale: "Grass", light: [ @@ -2262,7 +2016,7 @@ fn grass() -> ColorScaleSet { .unwrap() } -fn lime() -> ColorScaleSet { +pub(crate) fn lime() -> ColorScaleSet { StaticColorScaleSet { scale: "Lime", light: [ @@ -2326,7 +2080,7 @@ fn lime() -> ColorScaleSet { .unwrap() } -fn mint() -> ColorScaleSet { +pub(crate) fn mint() -> ColorScaleSet { StaticColorScaleSet { scale: "Mint", light: [ @@ -2390,7 +2144,7 @@ fn mint() -> ColorScaleSet { .unwrap() } -fn sky() -> ColorScaleSet { +pub(crate) fn sky() -> ColorScaleSet { StaticColorScaleSet { scale: "Sky", light: [ @@ -2454,7 +2208,7 @@ fn sky() -> ColorScaleSet { .unwrap() } -fn black() -> ColorScaleSet { +pub(crate) fn black() -> ColorScaleSet { StaticColorScaleSet { scale: "Black", light: [ @@ -2518,7 +2272,7 @@ fn black() -> ColorScaleSet { .unwrap() } -fn white() -> ColorScaleSet { +pub(crate) fn white() -> ColorScaleSet { StaticColorScaleSet { scale: "White", light: [ diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 40fb7df7cfd85d1ed33edcd16ad562af4519ee5d..8502f433f4a919d7d661f00e55d0dd353ff46fc5 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,58 +1,56 @@ -use std::sync::Arc; - use crate::{ - colors::{StatusColors, SystemColors, ThemeColors, ThemeStyles}, - default_color_scales, Appearance, PlayerColors, SyntaxTheme, Theme, ThemeFamily, + one_themes::{one_dark, one_family}, + Theme, ThemeFamily, }; -fn zed_pro_daylight() -> Theme { - Theme { - id: "zed_pro_daylight".to_string(), - name: "Zed Pro Daylight".into(), - appearance: Appearance::Light, - styles: ThemeStyles { - system: SystemColors::default(), - colors: ThemeColors::default_light(), - status: StatusColors::default(), - player: PlayerColors::default_light(), - syntax: Arc::new(SyntaxTheme::default_light()), - }, - } -} +// fn zed_pro_daylight() -> Theme { +// Theme { +// id: "zed_pro_daylight".to_string(), +// name: "Zed Pro Daylight".into(), +// appearance: Appearance::Light, +// styles: ThemeStyles { +// system: SystemColors::default(), +// colors: ThemeColors::light(), +// status: StatusColors::light(), +// player: PlayerColors::light(), +// syntax: Arc::new(SyntaxTheme::light()), +// }, +// } +// } -pub(crate) fn zed_pro_moonlight() -> Theme { - Theme { - id: "zed_pro_moonlight".to_string(), - name: "Zed Pro Moonlight".into(), - appearance: Appearance::Dark, - styles: ThemeStyles { - system: SystemColors::default(), - colors: ThemeColors::default_dark(), - status: StatusColors::default(), - player: PlayerColors::default(), - syntax: Arc::new(SyntaxTheme::default_dark()), - }, - } -} +// pub(crate) fn zed_pro_moonlight() -> Theme { +// Theme { +// id: "zed_pro_moonlight".to_string(), +// name: "Zed Pro Moonlight".into(), +// appearance: Appearance::Dark, +// styles: ThemeStyles { +// system: SystemColors::default(), +// colors: ThemeColors::dark(), +// status: StatusColors::dark(), +// player: PlayerColors::dark(), +// syntax: Arc::new(SyntaxTheme::dark()), +// }, +// } +// } -pub fn zed_pro_family() -> ThemeFamily { - ThemeFamily { - id: "zed_pro".to_string(), - name: "Zed Pro".into(), - author: "Zed Team".into(), - themes: vec![zed_pro_daylight(), zed_pro_moonlight()], - scales: default_color_scales(), - } -} +// pub fn zed_pro_family() -> ThemeFamily { +// ThemeFamily { +// id: "zed_pro".to_string(), +// name: "Zed Pro".into(), +// author: "Zed Team".into(), +// themes: vec![zed_pro_daylight(), zed_pro_moonlight()], +// scales: default_color_scales(), +// } +// } impl Default for ThemeFamily { fn default() -> Self { - zed_pro_family() + one_family() } } impl Default for Theme { fn default() -> Self { - zed_pro_daylight() + one_dark() } } diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs new file mode 100644 index 0000000000000000000000000000000000000000..6e32eace7350414b002dfda1d3bf242665401e74 --- /dev/null +++ b/crates/theme2/src/one_themes.rs @@ -0,0 +1,198 @@ +use std::sync::Arc; + +use gpui::{hsla, FontStyle, FontWeight, HighlightStyle}; + +use crate::{ + default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, + ThemeColors, ThemeFamily, ThemeStyles, +}; + +pub fn one_family() -> ThemeFamily { + ThemeFamily { + id: "one".to_string(), + name: "One".into(), + author: "".into(), + themes: vec![one_dark()], + scales: default_color_scales(), + } +} + +pub(crate) fn one_dark() -> Theme { + let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); + let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); + let elevated_surface = hsla(220. / 360., 12. / 100., 18. / 100., 1.); + + let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); + let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); + let green = hsla(95. / 360., 38. / 100., 62. / 100., 1.0); + let orange = hsla(29. / 360., 54. / 100., 61. / 100., 1.0); + let purple = hsla(286. / 360., 51. / 100., 64. / 100., 1.0); + let red = hsla(355. / 360., 65. / 100., 65. / 100., 1.0); + let teal = hsla(187. / 360., 47. / 100., 55. / 100., 1.0); + let yellow = hsla(39. / 360., 67. / 100., 69. / 100., 1.0); + + Theme { + id: "one_dark".to_string(), + name: "One Dark".into(), + appearance: Appearance::Dark, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors { + border: hsla(225. / 360., 13. / 100., 12. / 100., 1.), + border_variant: hsla(228. / 360., 8. / 100., 25. / 100., 1.), + border_focused: hsla(223. / 360., 78. / 100., 65. / 100., 1.), + border_selected: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + border_transparent: SystemColors::default().transparent, + border_disabled: hsla(222.0 / 360., 11.6 / 100., 33.7 / 100., 1.0), + elevated_surface_background: elevated_surface, + surface_background: bg, + background: bg, + element_background: elevated_surface, + element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), + element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0), + ghost_element_background: SystemColors::default().transparent, + ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), + ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + ghost_element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), + text: hsla(222.9 / 360., 9.1 / 100., 84.9 / 100., 1.0), + text_muted: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), + text_disabled: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), + text_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + icon: hsla(222.9 / 360., 9.9 / 100., 86.1 / 100., 1.0), + icon_muted: hsla(220.0 / 360., 12.1 / 100., 66.1 / 100., 1.0), + icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + icon_accent: blue.into(), + status_bar_background: bg, + title_bar_background: bg, + toolbar_background: editor, + tab_bar_background: bg, + tab_inactive_background: bg, + tab_active_background: editor, + editor_background: editor, + editor_gutter_background: editor, + editor_subheader_background: bg, + editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0), + editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1), + editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), + editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0), + editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), + editor_wrap_guide: gpui::black(), + editor_active_wrap_guide: gpui::red(), + editor_document_highlight_read_background: hsla( + 207.8 / 360., + 81. / 100., + 66. / 100., + 0.2, + ), + editor_document_highlight_write_background: gpui::red(), + terminal_background: bg, + // todo!("Use one colors for terminal") + terminal_ansi_black: crate::black().dark().step_12(), + terminal_ansi_red: crate::red().dark().step_11(), + terminal_ansi_green: crate::green().dark().step_11(), + terminal_ansi_yellow: crate::yellow().dark().step_11(), + terminal_ansi_blue: crate::blue().dark().step_11(), + terminal_ansi_magenta: crate::violet().dark().step_11(), + terminal_ansi_cyan: crate::cyan().dark().step_11(), + terminal_ansi_white: crate::neutral().dark().step_12(), + terminal_ansi_bright_black: crate::black().dark().step_11(), + terminal_ansi_bright_red: crate::red().dark().step_10(), + terminal_ansi_bright_green: crate::green().dark().step_10(), + terminal_ansi_bright_yellow: crate::yellow().dark().step_10(), + terminal_ansi_bright_blue: crate::blue().dark().step_10(), + terminal_ansi_bright_magenta: crate::violet().dark().step_10(), + terminal_ansi_bright_cyan: crate::cyan().dark().step_10(), + terminal_ansi_bright_white: crate::neutral().dark().step_11(), + }, + status: StatusColors { + conflict: yellow, + created: green, + deleted: red, + error: red, + hidden: gray, + hint: blue, + ignored: gray, + info: blue, + modified: yellow, + predictive: gray, + renamed: blue, + success: green, + unreachable: gray, + warning: yellow, + }, + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme { + highlights: vec![ + ("attribute".into(), purple.into()), + ("boolean".into(), orange.into()), + ("comment".into(), gray.into()), + ("comment.doc".into(), gray.into()), + ("constant".into(), yellow.into()), + ("constructor".into(), blue.into()), + ("embedded".into(), HighlightStyle::default()), + ( + "emphasis".into(), + HighlightStyle { + font_style: Some(FontStyle::Italic), + ..HighlightStyle::default() + }, + ), + ( + "emphasis.strong".into(), + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..HighlightStyle::default() + }, + ), + ("enum".into(), HighlightStyle::default()), + ("function".into(), blue.into()), + ("function.method".into(), blue.into()), + ("function.definition".into(), blue.into()), + ("hint".into(), blue.into()), + ("keyword".into(), purple.into()), + ("label".into(), HighlightStyle::default()), + ("link_text".into(), blue.into()), + ( + "link_uri".into(), + HighlightStyle { + color: Some(teal.into()), + font_style: Some(FontStyle::Italic), + ..HighlightStyle::default() + }, + ), + ("number".into(), orange.into()), + ("operator".into(), HighlightStyle::default()), + ("predictive".into(), HighlightStyle::default()), + ("preproc".into(), HighlightStyle::default()), + ("primary".into(), HighlightStyle::default()), + ("property".into(), red.into()), + ("punctuation".into(), HighlightStyle::default()), + ("punctuation.bracket".into(), HighlightStyle::default()), + ("punctuation.delimiter".into(), HighlightStyle::default()), + ("punctuation.list_marker".into(), HighlightStyle::default()), + ("punctuation.special".into(), HighlightStyle::default()), + ("string".into(), green.into()), + ("string.escape".into(), HighlightStyle::default()), + ("string.regex".into(), red.into()), + ("string.special".into(), HighlightStyle::default()), + ("string.special.symbol".into(), HighlightStyle::default()), + ("tag".into(), HighlightStyle::default()), + ("text.literal".into(), HighlightStyle::default()), + ("title".into(), HighlightStyle::default()), + ("type".into(), teal.into()), + ("variable".into(), HighlightStyle::default()), + ("variable.special".into(), red.into()), + ("variant".into(), HighlightStyle::default()), + ], + inlay_style: HighlightStyle::default(), + suggestion_style: HighlightStyle::default(), + }), + }, + } +} diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 0c61f6f22468901e6c7c78e8664d075c5c82779c..19af0ede51c1ce4467ac57d91dd8ffd86c8eb93c 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString}; use refineable::Refineable; use crate::{ - zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, - ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, + one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, + Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, }; pub struct ThemeRegistry { @@ -38,17 +38,17 @@ impl ThemeRegistry { fn insert_user_themes(&mut self, themes: impl IntoIterator) { self.insert_themes(themes.into_iter().map(|user_theme| { let mut theme_colors = match user_theme.appearance { - Appearance::Light => ThemeColors::default_light(), - Appearance::Dark => ThemeColors::default_dark(), + Appearance::Light => ThemeColors::light(), + Appearance::Dark => ThemeColors::dark(), }; theme_colors.refine(&user_theme.styles.colors); - let mut status_colors = StatusColors::default(); + let mut status_colors = StatusColors::dark(); status_colors.refine(&user_theme.styles.status); let mut syntax_colors = match user_theme.appearance { - Appearance::Light => SyntaxTheme::default_light(), - Appearance::Dark => SyntaxTheme::default_dark(), + Appearance::Light => SyntaxTheme::light(), + Appearance::Dark => SyntaxTheme::dark(), }; if let Some(user_syntax) = user_theme.styles.syntax { syntax_colors.highlights = user_syntax @@ -76,7 +76,10 @@ impl ThemeRegistry { system: SystemColors::default(), colors: theme_colors, status: status_colors, - player: PlayerColors::default(), + player: match user_theme.appearance { + Appearance::Light => PlayerColors::light(), + Appearance::Dark => PlayerColors::dark(), + }, syntax: Arc::new(syntax_colors), }, } @@ -105,7 +108,7 @@ impl Default for ThemeRegistry { themes: HashMap::default(), }; - this.insert_theme_families([zed_pro_family()]); + this.insert_theme_families([one_family()]); #[cfg(not(feature = "importing-themes"))] this.insert_user_theme_familes(crate::all_user_themes()); diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index c57576840119e42e42e3f305cd0dd4fc5463cf81..8a15b52641bc30ca03e4d32c65070b89d5b8e79c 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -1,3 +1,4 @@ +use crate::one_themes::one_dark; use crate::{Theme, ThemeRegistry}; use anyhow::Result; use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels}; @@ -129,7 +130,7 @@ impl settings::Settings for ThemeSettings { buffer_line_height: defaults.buffer_line_height.unwrap(), active_theme: themes .get(defaults.theme.as_ref().unwrap()) - .or(themes.get("Zed Pro Moonlight")) + .or(themes.get(&one_dark().name)) .unwrap(), }; diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs new file mode 100644 index 0000000000000000000000000000000000000000..18f9e76581fb3210da9174378f4fd2e314d19a3b --- /dev/null +++ b/crates/theme2/src/styles.rs @@ -0,0 +1,11 @@ +mod colors; +mod players; +mod status; +mod syntax; +mod system; + +pub use colors::*; +pub use players::*; +pub use status::*; +pub use syntax::*; +pub use system::*; diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/styles/colors.rs similarity index 94% rename from crates/theme2/src/colors.rs rename to crates/theme2/src/styles/colors.rs index b8cceebea820f9a7fe61d39bef2d32784006bbc4..1d4917ac00b99797c077623e13f2ac97bde87dd8 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/styles/colors.rs @@ -1,31 +1,8 @@ -use crate::{PlayerColors, SyntaxTheme}; use gpui::Hsla; use refineable::Refineable; use std::sync::Arc; -#[derive(Clone)] -pub struct SystemColors { - pub transparent: Hsla, - pub mac_os_traffic_light_red: Hsla, - pub mac_os_traffic_light_yellow: Hsla, - pub mac_os_traffic_light_green: Hsla, -} - -#[derive(Refineable, Clone, Debug)] -#[refineable(Debug, serde::Deserialize)] -pub struct StatusColors { - pub conflict: Hsla, - pub created: Hsla, - pub deleted: Hsla, - pub error: Hsla, - pub hidden: Hsla, - pub ignored: Hsla, - pub info: Hsla, - pub modified: Hsla, - pub renamed: Hsla, - pub success: Hsla, - pub warning: Hsla, -} +use crate::{PlayerColors, StatusColors, SyntaxTheme, SystemColors}; #[derive(Refineable, Clone, Debug)] #[refineable(Debug, serde::Deserialize)] @@ -259,7 +236,7 @@ mod tests { #[test] fn override_a_single_theme_color() { - let mut colors = ThemeColors::default_light(); + let mut colors = ThemeColors::light(); let magenta: Hsla = gpui::rgb(0xff00ff); @@ -277,7 +254,7 @@ mod tests { #[test] fn override_multiple_theme_colors() { - let mut colors = ThemeColors::default_light(); + let mut colors = ThemeColors::light(); let magenta: Hsla = gpui::rgb(0xff00ff); let green: Hsla = gpui::rgb(0x00ff00); diff --git a/crates/theme2/src/players.rs b/crates/theme2/src/styles/players.rs similarity index 66% rename from crates/theme2/src/players.rs rename to crates/theme2/src/styles/players.rs index 0e36ff594732c67ac7f1fd455e849aa259084bc7..68deceb0ff88aa60bebd35ddc202b6edf0aaccc8 100644 --- a/crates/theme2/src/players.rs +++ b/crates/theme2/src/styles/players.rs @@ -16,6 +16,107 @@ pub struct PlayerColor { #[derive(Clone)] pub struct PlayerColors(pub Vec); +impl Default for PlayerColors { + /// Don't use this! + /// We have to have a default to be `[refineable::Refinable]`. + /// todo!("Find a way to not need this for Refinable") + fn default() -> Self { + Self::dark() + } +} + +impl PlayerColors { + pub fn dark() -> Self { + Self(vec![ + PlayerColor { + cursor: blue().dark().step_9(), + background: blue().dark().step_5(), + selection: blue().dark().step_3(), + }, + PlayerColor { + cursor: orange().dark().step_9(), + background: orange().dark().step_5(), + selection: orange().dark().step_3(), + }, + PlayerColor { + cursor: pink().dark().step_9(), + background: pink().dark().step_5(), + selection: pink().dark().step_3(), + }, + PlayerColor { + cursor: lime().dark().step_9(), + background: lime().dark().step_5(), + selection: lime().dark().step_3(), + }, + PlayerColor { + cursor: purple().dark().step_9(), + background: purple().dark().step_5(), + selection: purple().dark().step_3(), + }, + PlayerColor { + cursor: amber().dark().step_9(), + background: amber().dark().step_5(), + selection: amber().dark().step_3(), + }, + PlayerColor { + cursor: jade().dark().step_9(), + background: jade().dark().step_5(), + selection: jade().dark().step_3(), + }, + PlayerColor { + cursor: red().dark().step_9(), + background: red().dark().step_5(), + selection: red().dark().step_3(), + }, + ]) + } + + pub fn light() -> Self { + Self(vec![ + PlayerColor { + cursor: blue().light().step_9(), + background: blue().light().step_4(), + selection: blue().light().step_3(), + }, + PlayerColor { + cursor: orange().light().step_9(), + background: orange().light().step_4(), + selection: orange().light().step_3(), + }, + PlayerColor { + cursor: pink().light().step_9(), + background: pink().light().step_4(), + selection: pink().light().step_3(), + }, + PlayerColor { + cursor: lime().light().step_9(), + background: lime().light().step_4(), + selection: lime().light().step_3(), + }, + PlayerColor { + cursor: purple().light().step_9(), + background: purple().light().step_4(), + selection: purple().light().step_3(), + }, + PlayerColor { + cursor: amber().light().step_9(), + background: amber().light().step_4(), + selection: amber().light().step_3(), + }, + PlayerColor { + cursor: jade().light().step_9(), + background: jade().light().step_4(), + selection: jade().light().step_3(), + }, + PlayerColor { + cursor: red().light().step_9(), + background: red().light().step_4(), + selection: red().light().step_3(), + }, + ]) + } +} + impl PlayerColors { pub fn local(&self) -> PlayerColor { // todo!("use a valid color"); @@ -36,6 +137,8 @@ impl PlayerColors { #[cfg(feature = "stories")] pub use stories::*; +use crate::{amber, blue, jade, lime, orange, pink, purple, red}; + #[cfg(feature = "stories")] mod stories { use super::*; diff --git a/crates/theme2/src/styles/status.rs b/crates/theme2/src/styles/status.rs new file mode 100644 index 0000000000000000000000000000000000000000..db0f4758250b613d2c544f6456bae5c302cba283 --- /dev/null +++ b/crates/theme2/src/styles/status.rs @@ -0,0 +1,134 @@ +use gpui::Hsla; +use refineable::Refineable; + +use crate::{blue, grass, neutral, red, yellow}; + +#[derive(Refineable, Clone, Debug)] +#[refineable(Debug, serde::Deserialize)] +pub struct StatusColors { + /// Indicates some kind of conflict, like a file changed on disk while it was open, or + /// merge conflicts in a Git repository. + pub conflict: Hsla, + + /// Indicates something new, like a new file added to a Git repository. + pub created: Hsla, + + /// Indicates that something no longer exists, like a deleted file. + pub deleted: Hsla, + + /// Indicates a system error, a failed operation or a diagnostic error. + pub error: Hsla, + + /// Represents a hidden status, such as a file being hidden in a file tree. + pub hidden: Hsla, + + /// Indicates a hint or some kind of additional information. + pub hint: Hsla, + + /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. + pub ignored: Hsla, + + /// Represents informational status updates or messages. + pub info: Hsla, + + /// Indicates a changed or altered status, like a file that has been edited. + pub modified: Hsla, + + /// Indicates something that is predicted, like automatic code completion, or generated code. + pub predictive: Hsla, + + /// Represents a renamed status, such as a file that has been renamed. + pub renamed: Hsla, + + /// Indicates a successful operation or task completion. + pub success: Hsla, + + /// Indicates some kind of unreachable status, like a block of code that can never be reached. + pub unreachable: Hsla, + + /// Represents a warning status, like an operation that is about to fail. + pub warning: Hsla, +} + +impl Default for StatusColors { + /// Don't use this! + /// We have to have a default to be `[refineable::Refinable]`. + /// todo!("Find a way to not need this for Refinable") + fn default() -> Self { + Self::dark() + } +} + +pub struct DiagnosticColors { + pub error: Hsla, + pub warning: Hsla, + pub info: Hsla, +} + +pub struct GitStatusColors { + pub created: Hsla, + pub deleted: Hsla, + pub modified: Hsla, + pub renamed: Hsla, + pub conflict: Hsla, + pub ignored: Hsla, +} + +impl StatusColors { + pub fn dark() -> Self { + Self { + conflict: red().dark().step_9(), + created: grass().dark().step_9(), + deleted: red().dark().step_9(), + error: red().dark().step_9(), + hidden: neutral().dark().step_9(), + hint: blue().dark().step_9(), + ignored: neutral().dark().step_9(), + info: blue().dark().step_9(), + modified: yellow().dark().step_9(), + predictive: neutral().dark_alpha().step_9(), + renamed: blue().dark().step_9(), + success: grass().dark().step_9(), + unreachable: neutral().dark().step_10(), + warning: yellow().dark().step_9(), + } + } + + pub fn light() -> Self { + Self { + conflict: red().light().step_9(), + created: grass().light().step_9(), + deleted: red().light().step_9(), + error: red().light().step_9(), + hidden: neutral().light().step_9(), + hint: blue().light().step_9(), + ignored: neutral().light().step_9(), + info: blue().light().step_9(), + modified: yellow().light().step_9(), + predictive: neutral().light_alpha().step_9(), + renamed: blue().light().step_9(), + success: grass().light().step_9(), + unreachable: neutral().light().step_10(), + warning: yellow().light().step_9(), + } + } + + pub fn diagnostic(&self) -> DiagnosticColors { + DiagnosticColors { + error: self.error, + warning: self.warning, + info: self.info, + } + } + + pub fn git(&self) -> GitStatusColors { + GitStatusColors { + created: self.created, + deleted: self.deleted, + modified: self.modified, + renamed: self.renamed, + conflict: self.conflict, + ignored: self.ignored, + } + } +} diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs new file mode 100644 index 0000000000000000000000000000000000000000..8675d30e3a00a94d3ea05efa018dfd7775dabace --- /dev/null +++ b/crates/theme2/src/styles/syntax.rs @@ -0,0 +1,170 @@ +use gpui::{HighlightStyle, Hsla}; + +use crate::{ + blue, cyan, gold, indigo, iris, jade, lime, mint, neutral, orange, plum, purple, red, sky, + tomato, yellow, +}; + +#[derive(Clone, Default)] +pub struct SyntaxTheme { + pub highlights: Vec<(String, HighlightStyle)>, + // todo!("Remove this in favor of StatusColor.hint") + // If this should be overridable we should move it to ThemeColors + pub inlay_style: HighlightStyle, + // todo!("Remove this in favor of StatusColor.prediction") + // If this should be overridable we should move it to ThemeColors + pub suggestion_style: HighlightStyle, +} + +impl SyntaxTheme { + pub fn light() -> Self { + Self { + highlights: vec![ + ("attribute".into(), cyan().light().step_11().into()), + ("boolean".into(), tomato().light().step_11().into()), + ("comment".into(), neutral().light().step_11().into()), + ("comment.doc".into(), iris().light().step_12().into()), + ("constant".into(), red().light().step_9().into()), + ("constructor".into(), red().light().step_9().into()), + ("embedded".into(), red().light().step_9().into()), + ("emphasis".into(), red().light().step_9().into()), + ("emphasis.strong".into(), red().light().step_9().into()), + ("enum".into(), red().light().step_9().into()), + ("function".into(), red().light().step_9().into()), + ("hint".into(), red().light().step_9().into()), + ("keyword".into(), orange().light().step_11().into()), + ("label".into(), red().light().step_9().into()), + ("link_text".into(), red().light().step_9().into()), + ("link_uri".into(), red().light().step_9().into()), + ("number".into(), red().light().step_9().into()), + ("operator".into(), red().light().step_9().into()), + ("predictive".into(), red().light().step_9().into()), + ("preproc".into(), red().light().step_9().into()), + ("primary".into(), red().light().step_9().into()), + ("property".into(), red().light().step_9().into()), + ("punctuation".into(), neutral().light().step_11().into()), + ( + "punctuation.bracket".into(), + neutral().light().step_11().into(), + ), + ( + "punctuation.delimiter".into(), + neutral().light().step_11().into(), + ), + ( + "punctuation.list_marker".into(), + blue().light().step_11().into(), + ), + ("punctuation.special".into(), red().light().step_9().into()), + ("string".into(), jade().light().step_11().into()), + ("string.escape".into(), red().light().step_9().into()), + ("string.regex".into(), tomato().light().step_11().into()), + ("string.special".into(), red().light().step_9().into()), + ( + "string.special.symbol".into(), + red().light().step_9().into(), + ), + ("tag".into(), red().light().step_9().into()), + ("text.literal".into(), red().light().step_9().into()), + ("title".into(), red().light().step_9().into()), + ("type".into(), red().light().step_9().into()), + ("variable".into(), red().light().step_9().into()), + ("variable.special".into(), red().light().step_9().into()), + ("variant".into(), red().light().step_9().into()), + ], + inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style") + suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style") + } + } + + pub fn dark() -> Self { + Self { + highlights: vec![ + ("attribute".into(), tomato().dark().step_11().into()), + ("boolean".into(), tomato().dark().step_11().into()), + ("comment".into(), neutral().dark().step_11().into()), + ("comment.doc".into(), iris().dark().step_12().into()), + ("constant".into(), orange().dark().step_11().into()), + ("constructor".into(), gold().dark().step_11().into()), + ("embedded".into(), red().dark().step_11().into()), + ("emphasis".into(), red().dark().step_11().into()), + ("emphasis.strong".into(), red().dark().step_11().into()), + ("enum".into(), yellow().dark().step_11().into()), + ("function".into(), blue().dark().step_11().into()), + ("hint".into(), indigo().dark().step_11().into()), + ("keyword".into(), plum().dark().step_11().into()), + ("label".into(), red().dark().step_11().into()), + ("link_text".into(), red().dark().step_11().into()), + ("link_uri".into(), red().dark().step_11().into()), + ("number".into(), red().dark().step_11().into()), + ("operator".into(), red().dark().step_11().into()), + ("predictive".into(), red().dark().step_11().into()), + ("preproc".into(), red().dark().step_11().into()), + ("primary".into(), red().dark().step_11().into()), + ("property".into(), red().dark().step_11().into()), + ("punctuation".into(), neutral().dark().step_11().into()), + ( + "punctuation.bracket".into(), + neutral().dark().step_11().into(), + ), + ( + "punctuation.delimiter".into(), + neutral().dark().step_11().into(), + ), + ( + "punctuation.list_marker".into(), + blue().dark().step_11().into(), + ), + ("punctuation.special".into(), red().dark().step_11().into()), + ("string".into(), lime().dark().step_11().into()), + ("string.escape".into(), orange().dark().step_11().into()), + ("string.regex".into(), tomato().dark().step_11().into()), + ("string.special".into(), red().dark().step_11().into()), + ( + "string.special.symbol".into(), + red().dark().step_11().into(), + ), + ("tag".into(), red().dark().step_11().into()), + ("text.literal".into(), purple().dark().step_11().into()), + ("title".into(), sky().dark().step_11().into()), + ("type".into(), mint().dark().step_11().into()), + ("variable".into(), red().dark().step_11().into()), + ("variable.special".into(), red().dark().step_11().into()), + ("variant".into(), red().dark().step_11().into()), + ], + inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style") + suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style") + } + } + + // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? + pub fn new_test(colors: impl IntoIterator) -> Self { + SyntaxTheme { + highlights: colors + .into_iter() + .map(|(key, color)| { + ( + key.to_owned(), + HighlightStyle { + color: Some(color), + ..Default::default() + }, + ) + }) + .collect(), + inlay_style: HighlightStyle::default(), + suggestion_style: HighlightStyle::default(), + } + } + + pub fn get(&self, name: &str) -> HighlightStyle { + self.highlights + .iter() + .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None }) + .unwrap_or_default() + } + + pub fn color(&self, name: &str) -> Hsla { + self.get(name).color.unwrap_or_default() + } +} diff --git a/crates/theme2/src/styles/system.rs b/crates/theme2/src/styles/system.rs new file mode 100644 index 0000000000000000000000000000000000000000..aeb0865155d68aa8e167421b1ce79895120203ff --- /dev/null +++ b/crates/theme2/src/styles/system.rs @@ -0,0 +1,20 @@ +use gpui::{hsla, Hsla}; + +#[derive(Clone)] +pub struct SystemColors { + pub transparent: Hsla, + pub mac_os_traffic_light_red: Hsla, + pub mac_os_traffic_light_yellow: Hsla, + pub mac_os_traffic_light_green: Hsla, +} + +impl Default for SystemColors { + fn default() -> Self { + Self { + transparent: hsla(0.0, 0.0, 0.0, 0.0), + mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), + mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), + mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), + } + } +} diff --git a/crates/theme2/src/syntax.rs b/crates/theme2/src/syntax.rs deleted file mode 100644 index 8aac238555f1190272a8f959da142901fb70d326..0000000000000000000000000000000000000000 --- a/crates/theme2/src/syntax.rs +++ /dev/null @@ -1,41 +0,0 @@ -use gpui::{HighlightStyle, Hsla}; - -#[derive(Clone, Default)] -pub struct SyntaxTheme { - pub highlights: Vec<(String, HighlightStyle)>, - pub inlay_style: HighlightStyle, - pub suggestion_style: HighlightStyle, -} - -impl SyntaxTheme { - // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? - pub fn new_test(colors: impl IntoIterator) -> Self { - SyntaxTheme { - highlights: colors - .into_iter() - .map(|(key, color)| { - ( - key.to_owned(), - HighlightStyle { - color: Some(color), - ..Default::default() - }, - ) - }) - .collect(), - inlay_style: HighlightStyle::default(), - suggestion_style: HighlightStyle::default(), - } - } - - pub fn get(&self, name: &str) -> HighlightStyle { - self.highlights - .iter() - .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None }) - .unwrap_or_default() - } - - pub fn color(&self, name: &str) -> Hsla { - self.get(name).color.unwrap_or_default() - } -} diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 7e2085de4e7249ccb1925f1f0c23555f81dd08fc..b6790b5a6ff534cb8080e7ae88a28c72cbeeefb7 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -1,11 +1,10 @@ -mod colors; mod default_colors; mod default_theme; -mod players; +mod one_themes; mod registry; mod scale; mod settings; -mod syntax; +mod styles; #[cfg(not(feature = "importing-themes"))] mod themes; mod user_theme; @@ -13,14 +12,12 @@ mod user_theme; use std::sync::Arc; use ::settings::Settings; -pub use colors::*; pub use default_colors::*; pub use default_theme::*; -pub use players::*; pub use registry::*; pub use scale::*; pub use settings::*; -pub use syntax::*; +pub use styles::*; #[cfg(not(feature = "importing-themes"))] pub use themes::*; pub use user_theme::*; diff --git a/crates/theme2/util/hex_to_hsla.py b/crates/theme2/util/hex_to_hsla.py new file mode 100644 index 0000000000000000000000000000000000000000..17faa186d8c6e6ae78ea907804a1a7b79f78bcfc --- /dev/null +++ b/crates/theme2/util/hex_to_hsla.py @@ -0,0 +1,35 @@ +import colorsys +import sys + +def hex_to_rgb(hex): + hex = hex.lstrip('#') + if len(hex) == 8: # 8 digit hex color + r, g, b, a = (int(hex[i:i+2], 16) for i in (0, 2, 4, 6)) + return r, g, b, a / 255.0 + else: # 6 digit hex color + return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) + (1.0,) + +def rgb_to_hsla(rgb): + h, l, s = colorsys.rgb_to_hls(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0) + a = rgb[3] # alpha value + return (round(h * 360, 1), round(s * 100, 1), round(l * 100, 1), round(a, 3)) + +def hex_to_hsla(hex): + return rgb_to_hsla(hex_to_rgb(hex)) + +if len(sys.argv) != 2: + print("Usage: python util/hex_to_hsla.py <6 or 8 digit hex color or comma-separated list of colors>") +else: + input_arg = sys.argv[1] + if ',' in input_arg: # comma-separated list of colors + hex_colors = input_arg.split(',') + hslas = [] # output array + for hex_color in hex_colors: + hex_color = hex_color.strip("'\" ") + h, s, l, a = hex_to_hsla(hex_color) + hslas.append(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") + print(hslas) + else: # single color + hex_color = input_arg.strip("'\"") + h, s, l, a = hex_to_hsla(hex_color) + print(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index bd02e694edd45561373f39e2673d7e290dcf30cb..b6c435c607da91f7f1d96b382a3a37bd6f452489 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -1,50 +1,42 @@ -use std::collections::HashSet; - -use strum::{EnumIter, IntoEnumIterator}; +use gpui::Action; +use strum::EnumIter; use crate::prelude::*; #[derive(Component)] -pub struct Keybinding { +pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. /// /// This should always contain at least one element. - keybinding: Vec<(String, ModifierKeys)>, + key_binding: gpui::KeyBinding, } -impl Keybinding { - pub fn new(key: String, modifiers: ModifierKeys) -> Self { - Self { - keybinding: vec![(key, modifiers)], - } +impl KeyBinding { + pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option { + // todo! this last is arbitrary, we want to prefer users key bindings over defaults, + // and vim over normal (in vim mode), etc. + let key_binding = cx.bindings_for_action(action).last().cloned()?; + Some(Self::new(key_binding)) } - pub fn new_chord( - first_note: (String, ModifierKeys), - second_note: (String, ModifierKeys), - ) -> Self { - Self { - keybinding: vec![first_note, second_note], - } + pub fn new(key_binding: gpui::KeyBinding) -> Self { + Self { key_binding } } fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { div() .flex() .gap_2() - .children(self.keybinding.iter().map(|(key, modifiers)| { + .children(self.key_binding.keystrokes().iter().map(|keystroke| { div() .flex() .gap_1() - .children(ModifierKey::iter().filter_map(|modifier| { - if modifiers.0.contains(&modifier) { - Some(Key::new(modifier.glyph().to_string())) - } else { - None - } - })) - .child(Key::new(key.clone())) + .when(keystroke.modifiers.control, |el| el.child(Key::new("^"))) + .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥"))) + .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘"))) + .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧"))) + .child(Key::new(keystroke.key.clone())) })) } } @@ -81,76 +73,6 @@ pub enum ModifierKey { Shift, } -impl ModifierKey { - /// Returns the glyph for the [`ModifierKey`]. - pub fn glyph(&self) -> char { - match self { - Self::Control => '^', - Self::Alt => '⌥', - Self::Command => '⌘', - Self::Shift => '⇧', - } - } -} - -#[derive(Clone)] -pub struct ModifierKeys(HashSet); - -impl ModifierKeys { - pub fn new() -> Self { - Self(HashSet::new()) - } - - pub fn all() -> Self { - Self(HashSet::from_iter(ModifierKey::iter())) - } - - pub fn add(mut self, modifier: ModifierKey) -> Self { - self.0.insert(modifier); - self - } - - pub fn control(mut self, control: bool) -> Self { - if control { - self.0.insert(ModifierKey::Control); - } else { - self.0.remove(&ModifierKey::Control); - } - - self - } - - pub fn alt(mut self, alt: bool) -> Self { - if alt { - self.0.insert(ModifierKey::Alt); - } else { - self.0.remove(&ModifierKey::Alt); - } - - self - } - - pub fn command(mut self, command: bool) -> Self { - if command { - self.0.insert(ModifierKey::Command); - } else { - self.0.remove(&ModifierKey::Command); - } - - self - } - - pub fn shift(mut self, shift: bool) -> Self { - if shift { - self.0.insert(ModifierKey::Shift); - } else { - self.0.remove(&ModifierKey::Shift); - } - - self - } -} - #[cfg(feature = "stories")] pub use stories::*; @@ -158,29 +80,38 @@ pub use stories::*; mod stories { use super::*; use crate::Story; - use gpui::{Div, Render}; + use gpui::{action, Div, Render}; use itertools::Itertools; pub struct KeybindingStory; + #[action] + struct NoAction {} + + pub fn binding(key: &str) -> gpui::KeyBinding { + gpui::KeyBinding::new(key, NoAction {}, None) + } + impl Render for KeybindingStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let all_modifier_permutations = ModifierKey::iter().permutations(2); + let all_modifier_permutations = + ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2); Story::container(cx) - .child(Story::title_for::<_, Keybinding>(cx)) + .child(Story::title_for::<_, KeyBinding>(cx)) .child(Story::label(cx, "Single Key")) - .child(Keybinding::new("Z".to_string(), ModifierKeys::new())) + .child(KeyBinding::new(binding("Z"))) .child(Story::label(cx, "Single Key with Modifier")) .child( div() .flex() .gap_3() - .children(ModifierKey::iter().map(|modifier| { - Keybinding::new("C".to_string(), ModifierKeys::new().add(modifier)) - })), + .child(KeyBinding::new(binding("ctrl-c"))) + .child(KeyBinding::new(binding("alt-c"))) + .child(KeyBinding::new(binding("cmd-c"))) + .child(KeyBinding::new(binding("shift-c"))), ) .child(Story::label(cx, "Single Key with Modifier (Permuted)")) .child( @@ -194,29 +125,17 @@ mod stories { .gap_4() .py_3() .children(chunk.map(|permutation| { - let mut modifiers = ModifierKeys::new(); - - for modifier in permutation { - modifiers = modifiers.add(modifier); - } - - Keybinding::new("X".to_string(), modifiers) + KeyBinding::new(binding(&*(permutation.join("-") + "-x"))) })) }), ), ) .child(Story::label(cx, "Single Key with All Modifiers")) - .child(Keybinding::new("Z".to_string(), ModifierKeys::all())) + .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z"))) .child(Story::label(cx, "Chord")) - .child(Keybinding::new_chord( - ("A".to_string(), ModifierKeys::new()), - ("Z".to_string(), ModifierKeys::new()), - )) + .child(KeyBinding::new(binding("a z"))) .child(Story::label(cx, "Chord with Modifier")) - .child(Keybinding::new_chord( - ("A".to_string(), ModifierKeys::new().control(true)), - ("Z".to_string(), ModifierKeys::new().shift(true)), - )) + .child(KeyBinding::new(binding("ctrl-a shift-z"))) } } } diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 7f736433fcd4d376534e265d3bf412aaa068888c..4e1034595db8043bae80823fb793509c86f01ff0 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, v_stack, KeyBinding, Label, LabelColor}; #[derive(Component)] pub struct Palette { @@ -108,7 +108,7 @@ impl Palette { pub struct PaletteItem { pub label: SharedString, pub sublabel: Option, - pub keybinding: Option, + pub keybinding: Option, } impl PaletteItem { @@ -132,7 +132,7 @@ impl PaletteItem { pub fn keybinding(mut self, keybinding: K) -> Self where - K: Into>, + K: Into>, { self.keybinding = keybinding.into(); self @@ -161,7 +161,7 @@ pub use stories::*; mod stories { use gpui::{Div, Render}; - use crate::{ModifierKeys, Story}; + use crate::{binding, Story}; use super::*; @@ -181,46 +181,24 @@ mod stories { Palette::new("palette-2") .placeholder("Execute a command...") .items(vec![ - PaletteItem::new("theme selector: toggle").keybinding( - Keybinding::new_chord( - ("k".to_string(), ModifierKeys::new().command(true)), - ("t".to_string(), ModifierKeys::new().command(true)), - ), - ), - PaletteItem::new("assistant: inline assist").keybinding( - Keybinding::new( - "enter".to_string(), - ModifierKeys::new().command(true), - ), - ), - PaletteItem::new("assistant: quote selection").keybinding( - Keybinding::new( - ">".to_string(), - ModifierKeys::new().command(true), - ), - ), - PaletteItem::new("assistant: toggle focus").keybinding( - Keybinding::new( - "?".to_string(), - ModifierKeys::new().command(true), - ), - ), + PaletteItem::new("theme selector: toggle") + .keybinding(KeyBinding::new(binding("cmd-k cmd-t"))), + PaletteItem::new("assistant: inline assist") + .keybinding(KeyBinding::new(binding("cmd-enter"))), + PaletteItem::new("assistant: quote selection") + .keybinding(KeyBinding::new(binding("cmd-<"))), + PaletteItem::new("assistant: toggle focus") + .keybinding(KeyBinding::new(binding("cmd-?"))), PaletteItem::new("auto update: check"), PaletteItem::new("auto update: view release notes"), - PaletteItem::new("branches: open recent").keybinding( - Keybinding::new( - "b".to_string(), - ModifierKeys::new().command(true).alt(true), - ), - ), + PaletteItem::new("branches: open recent") + .keybinding(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( - "escape".to_string(), - ModifierKeys::new(), - )), + PaletteItem::new("editor: cancel") + .keybinding(KeyBinding::new(binding("escape"))), ]), ) } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 87860ce943135997c8a0a816d81d44d5a500fb25..e6c0e3f44daf8e301cb543fb7203d1a5fed4738c 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,6 +1,8 @@ use gpui::{div, Div, ParentElement, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; +use crate::StyledExt; + #[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, @@ -16,16 +18,13 @@ impl Render for TextTooltip { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let theme = cx.theme(); div() - .bg(theme.colors().background) - .rounded_lg() - .border() + .elevation_2(cx) .font("Zed Sans") - .border_color(theme.colors().border) - .text_color(theme.colors().text) - .pl_2() - .pr_2() + .text_ui() + .text_color(cx.theme().colors().text) + .py_1() + .px_2() .child(self.title.clone()) } } diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index ffdd3fee9849147719f89dcf08c23bff17dee4d9..89aef8140a062685b59f00c508d99fea5fe19b1b 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -7,12 +7,12 @@ use gpui::{AppContext, ViewContext}; use rand::Rng; use theme2::ActiveTheme; -use crate::HighlightedText; +use crate::{binding, HighlightedText}; use crate::{ Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, - HighlightedLine, Icon, Keybinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, - MicStatus, ModifierKeys, Notification, PaletteItem, Player, PlayerCallStatus, - PlayerWithCallStatus, PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, + HighlightedLine, Icon, KeyBinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, + MicStatus, Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, + PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, }; use crate::{ListItem, NotificationAction}; @@ -701,46 +701,16 @@ pub fn static_collab_panel_channels() -> Vec { pub fn example_editor_actions() -> Vec { vec![ - PaletteItem::new("New File").keybinding(Keybinding::new( - "N".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Open File").keybinding(Keybinding::new( - "O".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Save File").keybinding(Keybinding::new( - "S".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Cut").keybinding(Keybinding::new( - "X".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Copy").keybinding(Keybinding::new( - "C".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Paste").keybinding(Keybinding::new( - "V".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Undo").keybinding(Keybinding::new( - "Z".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Redo").keybinding(Keybinding::new( - "Z".to_string(), - ModifierKeys::new().command(true).shift(true), - )), - PaletteItem::new("Find").keybinding(Keybinding::new( - "F".to_string(), - ModifierKeys::new().command(true), - )), - PaletteItem::new("Replace").keybinding(Keybinding::new( - "R".to_string(), - ModifierKeys::new().command(true), - )), + 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("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 3d6af476a4868f867aa8b718b67231e2b6ae0744..0407d98f862abc37d319bb0ad61204a43ff3a451 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -1,4 +1,4 @@ -use gpui::{Div, ElementFocus, ElementInteractivity, Styled, UniformList, ViewContext}; +use gpui::{Div, ElementInteractivity, KeyDispatch, Styled, UniformList, ViewContext}; use theme2::ActiveTheme; use crate::{ElevationIndex, UITextSize}; @@ -96,7 +96,7 @@ pub trait StyledExt: Styled + Sized { impl StyledExt for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2bba684d12c67c2477299ed5915b80a65e7de2d4..d0613e13abc6f63ae9a323bd873400b751063a40 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1401,20 +1401,32 @@ impl Pane { // .on_drop(|_view, state: View, cx| { // eprintln!("{:?}", state.read(cx)); // }) - .px_2() - .py_0p5() .flex() .items_center() .justify_center() + // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize") + .map(|this| { + if close_right { + this.pl_3().pr_1() + } else { + this.pr_1().pr_3() + } + }) + .py_1() .bg(tab_bg) - .hover(|h| h.bg(tab_hover_bg)) - .active(|a| a.bg(tab_active_bg)) + .border_color(cx.theme().colors().border) + .map(|this| match ix.cmp(&self.active_item_index) { + cmp::Ordering::Less => this.border_l(), + cmp::Ordering::Equal => this.border_r(), + cmp::Ordering::Greater => this.border_l().border_r(), + }) + // .hover(|h| h.bg(tab_hover_bg)) + // .active(|a| a.bg(tab_active_bg)) .child( div() - .px_1() .flex() .items_center() - .gap_1p5() + .gap_1() .text_color(text_color) .children(if item.has_conflict(cx) { Some( diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 0edc66d0d7d04c528dcf94e6269b41ac2e7097a2..2ddd210c9f1eb2cb330140b0b2a9cb0a88311739 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -37,10 +37,10 @@ use futures::{ }; use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, - AsyncAppContext, AsyncWindowContext, Bounds, Component, DispatchContext, Div, Entity, EntityId, - EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, - Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription, - Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + 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, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; @@ -534,8 +534,8 @@ pub struct Workspace { workspace_actions: Vec< Box< dyn Fn( - Div>, - ) -> Div>, + Div>, + ) -> Div>, >, >, zoomed: Option, @@ -3517,8 +3517,8 @@ impl Workspace { fn add_workspace_actions_listeners( &self, - mut div: Div>, - ) -> Div> { + mut div: Div>, + ) -> Div> { for action in self.workspace_actions.iter() { div = (action)(div) } @@ -3746,158 +3746,159 @@ impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let mut context = DispatchContext::default(); - context.insert("Workspace"); - cx.with_key_dispatch_context(context, |cx| { - div() - .relative() - .size_full() - .flex() - .flex_col() - .font("Zed Sans") - .gap_0() - .justify_start() - .items_start() - .text_color(cx.theme().colors().text) - .bg(cx.theme().colors().background) - .child(self.render_titlebar(cx)) - .child( - // todo! should this be a component a view? - self.add_workspace_actions_listeners(div().id("workspace")) - .relative() - .flex_1() - .w_full() - .flex() - .overflow_hidden() - .border_t() - .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()), - // ), + let mut context = KeyContext::default(); + context.add("Workspace"); + + self.add_workspace_actions_listeners(div()) + .context(context) + .relative() + .size_full() + .flex() + .flex_col() + .font("Zed Sans") + .gap_0() + .justify_start() + .items_start() + .text_color(cx.theme().colors().text) + .bg(cx.theme().colors().background) + .child(self.render_titlebar(cx)) + .child( + // todo! should this be a component a view? + div() + .id("workspace") + .relative() + .flex_1() + .w_full() + .flex() + .overflow_hidden() + .border_t() + .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("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(), - // ), - // ])), + // Panel::new("terminal-panel", cx) + // .child(Terminal::new()) + // .allowed_sides(PanelAllowedSides::BottomOnly) + // .side(PanelSide::Bottom), // ) - // .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()), + // .filter(|_| self.is_terminal_open()), // ), - ) - .child(self.status_bar.clone()) - // .when(self.debug.show_toast, |this| { - // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) - // }) - // .children( - // Some( - // div() - // .absolute() - // .top(px(50.)) - // .left(px(640.)) - // .z_index(8) - // .child(LanguageSelector::new("language-selector")), - // ) - // .filter(|_| self.is_language_selector_open()), - // ) - .z_index(8) - // Debug - .child( - div() - .flex() - .flex_col() - .z_index(9) - .absolute() - .top_20() - .left_1_4() - .w_40() - .gap_2(), // .when(self.show_debug, |this| { - // this.child(Button::::new("Toggle User Settings").on_click( - // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), - // )) - // .child( - // Button::::new("Toggle Toasts").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_toast(cx), - // )), - // ) - // .child( - // Button::::new("Toggle Livestream").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_livestream(cx), - // )), - // ) - // }) - // .child( - // Button::::new("Toggle Debug") - // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), - // ), - ) - }) + ), // .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()), + // ), + ) + .child(self.status_bar.clone()) + // .when(self.debug.show_toast, |this| { + // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) + // }) + // .children( + // Some( + // div() + // .absolute() + // .top(px(50.)) + // .left(px(640.)) + // .z_index(8) + // .child(LanguageSelector::new("language-selector")), + // ) + // .filter(|_| self.is_language_selector_open()), + // ) + .z_index(8) + // Debug + .child( + div() + .flex() + .flex_col() + .z_index(9) + .absolute() + .top_20() + .left_1_4() + .w_40() + .gap_2(), // .when(self.show_debug, |this| { + // this.child(Button::::new("Toggle User Settings").on_click( + // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), + // )) + // .child( + // Button::::new("Toggle Toasts").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_toast(cx), + // )), + // ) + // .child( + // Button::::new("Toggle Livestream").on_click(Arc::new( + // |workspace, cx| workspace.debug_toggle_livestream(cx), + // )), + // ) + // }) + // .child( + // Button::::new("Toggle Debug") + // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), + // ), + ) } } // todo!()