From 74a0d9316a850f7e39c9a6a9603831129600d594 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 10 Nov 2023 11:56:14 -0700 Subject: [PATCH 01/32] Add a DispatchTree which will replace the existing key dispatch strategy Instead of freezing a stack, we will record the entire dispatch tree so we can change focus. Co-Authored-By: Antonio Scandurra --- crates/editor2/src/editor.rs | 22 +- crates/editor2/src/element.rs | 28 +- crates/gpui/src/dispatch.rs | 1 + crates/gpui2/src/action.rs | 400 +------------------------ crates/gpui2/src/dispatch.rs | 225 ++++++++++++++ crates/gpui2/src/gpui2.rs | 1 + crates/gpui2/src/interactive.rs | 28 +- crates/gpui2/src/keymap/binding.rs | 12 +- crates/gpui2/src/keymap/context.rs | 434 ++++++++++++++++++++++++++++ crates/gpui2/src/keymap/keymap.rs | 4 +- crates/gpui2/src/keymap/matcher.rs | 10 +- crates/gpui2/src/keymap/mod.rs | 2 + crates/gpui2/src/window.rs | 34 +-- crates/workspace2/src/workspace2.rs | 14 +- 14 files changed, 724 insertions(+), 491 deletions(-) create mode 100644 crates/gpui/src/dispatch.rs create mode 100644 crates/gpui2/src/dispatch.rs create mode 100644 crates/gpui2/src/keymap/context.rs diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 5bc67a57b61feb5074852bb4a4b567c121a0f968..ad17f96055f560210790f783894b18774eec0fab 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, KeyBindingContext, 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, @@ -1980,9 +1980,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) -> KeyBindingContext { + let mut dispatch_context = KeyBindingContext::default(); + dispatch_context.add("Editor"); let mode = match self.mode { EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", @@ -1990,17 +1990,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 67fcbaa4ba11acf260ffad4c29f7f9c217d1f727..2cd319f66bc4d616437b42ba2f7bf72396a6ca83 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -16,11 +16,12 @@ 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, + KeyBindingContext, KeyDownEvent, KeyListener, 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; @@ -4157,21 +4158,6 @@ fn build_key_listeners( 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 - }, - ), ] } @@ -4179,7 +4165,7 @@ fn build_key_listener( listener: impl Fn( &mut Editor, &T, - &[&DispatchContext], + &[&KeyBindingContext], DispatchPhase, &mut ViewContext, ) -> Option> 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/dispatch.rs b/crates/gpui2/src/dispatch.rs new file mode 100644 index 0000000000000000000000000000000000000000..372c8c26104e7cabbffb9ba703af976025faadc3 --- /dev/null +++ b/crates/gpui2/src/dispatch.rs @@ -0,0 +1,225 @@ +use crate::{ + Action, DispatchPhase, FocusId, KeyBindingContext, KeyDownEvent, KeyMatch, Keymap, + KeystrokeMatcher, WindowContext, +}; +use collections::HashMap; +use parking_lot::Mutex; +use smallvec::SmallVec; +use std::{any::Any, sync::Arc}; + +// trait KeyListener -> FnMut(&E, &mut V, &mut ViewContext) +type AnyKeyListener = Box; +type AnyActionListener = Box; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct DispatchNodeId(usize); + +pub struct DispatchTree { + node_stack: Vec, + context_stack: Vec, + nodes: Vec, + focused: Option, + focusable_node_ids: HashMap, + keystroke_matchers: HashMap, KeystrokeMatcher>, + keymap: Arc>, +} + +#[derive(Default)] +pub struct DispatchNode { + key_listeners: SmallVec<[AnyKeyListener; 2]>, + action_listeners: SmallVec<[AnyActionListener; 16]>, + context: KeyBindingContext, + parent: Option, +} + +impl DispatchTree { + pub fn clear(&mut self) { + self.node_stack.clear(); + self.nodes.clear(); + } + + pub fn push_node(&mut self, context: Option, old_tree: &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 let Some(context) = context { + self.context_stack.push(context); + if let Some((context_stack, matcher)) = old_tree + .keystroke_matchers + .remove_entry(self.context_stack.as_slice()) + { + self.keystroke_matchers.insert(context_stack, matcher); + } + } + } + + pub fn pop_node(&mut self) -> DispatchNodeId { + self.node_stack.pop().unwrap() + } + + pub fn on_key_event(&mut self, listener: AnyKeyListener) { + self.active_node().key_listeners.push(listener); + } + + pub fn on_action(&mut self, listener: AnyActionListener) { + self.active_node().action_listeners.push(listener); + } + + pub fn make_focusable(&mut self, focus_id: FocusId) { + self.focusable_node_ids + .insert(focus_id, self.active_node_id()); + } + + pub fn set_focus(&mut self, focus_id: Option) { + self.focused = focus_id; + } + + pub fn active_node(&mut self) -> &mut DispatchNode { + let node_id = self.active_node_id(); + &mut self.nodes[node_id.0] + } + + fn active_node_id(&self) -> DispatchNodeId { + *self.node_stack.last().unwrap() + } + + /// Returns the DispatchNodeIds from the root of the tree to the given target node id. + 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 dispatch_key(&mut self, event: &dyn Any, cx: &mut WindowContext) { + if let Some(focused_node_id) = self + .focused + .and_then(|focus_id| self.focusable_node_ids.get(&focus_id)) + .copied() + { + self.dispatch_key_on_node(focused_node_id, event, cx); + } + } + + fn dispatch_key_on_node( + &mut self, + node_id: DispatchNodeId, + event: &dyn Any, + cx: &mut WindowContext, + ) { + let dispatch_path = self.dispatch_path(node_id); + + // Capture phase + self.context_stack.clear(); + cx.propagate_event = true; + for node_id in &dispatch_path { + let node = &self.nodes[node_id.0]; + if !node.context.is_empty() { + self.context_stack.push(node.context.clone()); + } + + for key_listener in &node.key_listeners { + key_listener(event, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = &self.nodes[node_id.0]; + + // Handle low level key events + for key_listener in &node.key_listeners { + key_listener(event, DispatchPhase::Bubble, cx); + if !cx.propagate_event { + return; + } + } + + // Match keystrokes + if !node.context.is_empty() { + if let Some(key_down_event) = event.downcast_ref::() { + if !self + .keystroke_matchers + .contains_key(self.context_stack.as_slice()) + { + let keystroke_contexts = self.context_stack.iter().cloned().collect(); + self.keystroke_matchers.insert( + keystroke_contexts, + KeystrokeMatcher::new(self.keymap.clone()), + ); + } + + if let Some(keystroke_matcher) = self + .keystroke_matchers + .get_mut(self.context_stack.as_slice()) + { + if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke( + &key_down_event.keystroke, + self.context_stack.as_slice(), + ) { + self.dispatch_action_on_node(*node_id, action, cx); + if !cx.propagate_event { + return; + } + } + } + } + + self.context_stack.pop(); + } + } + } + + pub fn dispatch_action(&self, action: Box, cx: &mut WindowContext) { + if let Some(focused_node_id) = self + .focused + .and_then(|focus_id| self.focusable_node_ids.get(&focus_id)) + .copied() + { + self.dispatch_action_on_node(focused_node_id, action, cx); + } + } + + fn dispatch_action_on_node( + &self, + node_id: DispatchNodeId, + action: Box, + cx: &mut WindowContext, + ) { + let dispatch_path = self.dispatch_path(node_id); + + // Capture phase + for node_id in &dispatch_path { + let node = &self.nodes[node_id.0]; + for action_listener in &node.action_listeners { + action_listener(&action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = &self.nodes[node_id.0]; + for action_listener in &node.action_listeners { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + action_listener(&action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + } +} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 79275005d21423071e780624090bc651e094415f..42aea446f1e4bac7c7eaf2399ff72573b9f61242 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -3,6 +3,7 @@ mod action; mod app; mod assets; mod color; +mod dispatch; mod element; mod elements; mod executor; diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 243eb3cb07844a2fa558d6c1bf2555f75cf1af95..946a59a809b0a758cd74e1ae1bc1c558a53a3704 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,6 +1,6 @@ use crate::{ div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, BorrowWindow, Bounds, - Component, DispatchContext, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyMatch, + Component, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyBindingContext, KeyMatch, Keystroke, Modifiers, Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, Task, View, ViewContext, }; @@ -167,7 +167,7 @@ pub trait StatelessInteractive: Element { fn context(mut self, context: C) -> Self where Self: Sized, - C: TryInto, + C: TryInto, C::Error: Debug, { self.stateless_interactivity().dispatch_context = @@ -403,24 +403,6 @@ pub trait ElementInteractivity: 'static { ) -> 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) }) @@ -808,7 +790,7 @@ impl ElementInteractivity for StatefulInteractivity { type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; pub struct StatelessInteractivity { - pub dispatch_context: DispatchContext, + pub dispatch_context: KeyBindingContext, pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, @@ -910,7 +892,7 @@ impl InteractiveElementState { impl Default for StatelessInteractivity { fn default() -> Self { Self { - dispatch_context: DispatchContext::default(), + dispatch_context: KeyBindingContext::default(), mouse_down_listeners: SmallVec::new(), mouse_up_listeners: SmallVec::new(), mouse_move_listeners: SmallVec::new(), @@ -1254,7 +1236,7 @@ pub type KeyListener = Box< dyn Fn( &mut V, &dyn Any, - &[&DispatchContext], + &[&KeyBindingContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 829f7a3b2cfa7a816b76ebc7c1acd3229b57aa18..1cf62484b98c943d33227853b9f2d16f883aae94 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -1,11 +1,11 @@ -use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke}; +use crate::{Action, KeyBindingContext, KeyBindingContextPredicate, 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(super) context_predicate: Option, } impl KeyBinding { @@ -15,7 +15,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 +32,7 @@ impl KeyBinding { }) } - pub fn matches_context(&self, contexts: &[&DispatchContext]) -> bool { + pub fn matches_context(&self, contexts: &[KeyBindingContext]) -> bool { self.context_predicate .as_ref() .map(|predicate| predicate.eval(contexts)) @@ -42,7 +42,7 @@ impl KeyBinding { pub fn match_keystrokes( &self, pending_keystrokes: &[Keystroke], - contexts: &[&DispatchContext], + contexts: &[KeyBindingContext], ) -> KeyMatch { if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) @@ -61,7 +61,7 @@ impl KeyBinding { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[&DispatchContext], + contexts: &[KeyBindingContext], ) -> 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..834bd4989a463d01b196a523aacd53869163da1c --- /dev/null +++ b/crates/gpui2/src/keymap/context.rs @@ -0,0 +1,434 @@ +use crate::SharedString; +use anyhow::{anyhow, Result}; +use smallvec::SmallVec; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)] +pub struct KeyBindingContext(SmallVec<[ContextEntry; 8]>); + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +struct ContextEntry { + key: SharedString, + value: Option, +} + +impl<'a> TryFrom<&'a str> for KeyBindingContext { + type Error = anyhow::Error; + + fn try_from(value: &'a str) -> Result { + Self::parse(value) + } +} + +impl KeyBindingContext { + 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() + } +} + +#[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: &[KeyBindingContext]) -> 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 = KeyBindingContext::default(); + expected.set("foo", "bar"); + expected.add("baz"); + assert_eq!(KeyBindingContext::parse("baz foo=bar").unwrap(), expected); + assert_eq!(KeyBindingContext::parse("foo = bar baz").unwrap(), expected); + assert_eq!( + KeyBindingContext::parse(" baz foo = bar baz").unwrap(), + expected + ); + assert_eq!( + KeyBindingContext::parse(" foo = bar baz").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..c9b5d26ecbce22d98e001a3b815759fd8605cd9c 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, KeyBindingContext, 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: &[KeyBindingContext], ) -> 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: &[KeyBindingContext], ) -> 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/window.rs b/crates/gpui2/src/window.rs index fd3890d644e7890334c2cdbe629ee661ec8bc0b4..cde7b31754d295739ad91f04dfda38db6ebd9b95 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, 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, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, + FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, + KeyBindingContext, KeyListener, KeyMatch, Keystroke, KeystrokeMatcher, 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, Result}; use collections::HashMap; @@ -64,7 +64,7 @@ type AnyListener = Box Option> @@ -230,7 +230,7 @@ pub struct Window { #[derive(Default)] pub(crate) struct Frame { element_states: HashMap, - key_matchers: HashMap, + key_matchers: HashMap, mouse_listeners: HashMap>, pub(crate) focus_listeners: Vec, pub(crate) key_dispatch_stack: Vec, @@ -337,7 +337,7 @@ pub(crate) enum KeyDispatchStackFrame { event_type: TypeId, listener: AnyKeyListener, }, - Context(DispatchContext), + Context(KeyBindingContext), } /// Indicates which region of the window is visible. Content falling outside of this mask will not be @@ -1228,7 +1228,7 @@ impl<'a> WindowContext<'a> { } 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(); + let mut context_stack = SmallVec::<[&KeyBindingContext; 16]>::new(); for (ix, frame) in key_dispatch_stack.iter().enumerate() { match frame { @@ -1300,7 +1300,7 @@ impl<'a> WindowContext<'a> { &mut self, element_id: &GlobalElementId, keystroke: &Keystroke, - context_stack: &[&DispatchContext], + context_stack: &[KeyBindingContext], ) -> KeyMatch { let key_match = self .window @@ -1621,7 +1621,7 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { .previous_frame .key_matchers .remove(&global_id) - .unwrap_or_else(|| KeyMatcher::new(keymap)), + .unwrap_or_else(|| KeystrokeMatcher::new(keymap)), ); } @@ -2120,7 +2120,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { let handle = self.view().downgrade(); let listener = Box::new( move |event: &dyn Any, - context_stack: &[&DispatchContext], + context_stack: &[&KeyBindingContext], phase: DispatchPhase, cx: &mut WindowContext<'_>| { handle @@ -2154,7 +2154,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { pub fn with_key_dispatch_context( &mut self, - context: DispatchContext, + context: KeyBindingContext, f: impl FnOnce(&mut Self) -> R, ) -> R { if context.is_empty() { diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5c678df317ac9a97e858f8e0ba3be8e9fdb89b6c..1522b4ec4e6142ff440a32842c8f0229c5eb350f 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -37,11 +37,11 @@ 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, - WindowOptions, + AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, + FocusHandle, GlobalPixels, KeyBindingContext, Model, ModelContext, ParentElement, Point, + Render, Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3743,8 +3743,8 @@ impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let mut context = DispatchContext::default(); - context.insert("Workspace"); + let mut context = KeyBindingContext::default(); + context.add("Workspace"); cx.with_key_dispatch_context(context, |cx| { div() .relative() From 7eaba8fabc8f7e3d28459b713f352fb75a2663e3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 10 Nov 2023 14:47:45 -0700 Subject: [PATCH 02/32] WIP --- crates/editor2/src/editor.rs | 8 +- crates/editor2/src/element.rs | 19 +- crates/gpui2/src/dispatch.rs | 225 ---------- crates/gpui2/src/elements/div.rs | 95 +++-- crates/gpui2/src/elements/img.rs | 26 +- crates/gpui2/src/elements/svg.rs | 28 +- crates/gpui2/src/focusable.rs | 252 ------------ crates/gpui2/src/gpui2.rs | 6 +- crates/gpui2/src/interactive.rs | 44 +- crates/gpui2/src/key_dispatch.rs | 547 +++++++++++++++++++++++++ crates/gpui2/src/keymap/binding.rs | 8 +- crates/gpui2/src/keymap/context.rs | 21 +- crates/gpui2/src/keymap/matcher.rs | 6 +- crates/gpui2/src/prelude.rs | 1 + crates/gpui2/src/view.rs | 4 - crates/gpui2/src/window.rs | 449 ++++---------------- crates/picker2/src/picker2.rs | 8 +- crates/storybook2/src/stories/focus.rs | 4 +- crates/ui2/src/styled_ext.rs | 4 +- crates/workspace2/src/workspace2.rs | 302 +++++++------- 20 files changed, 907 insertions(+), 1150 deletions(-) delete mode 100644 crates/gpui2/src/dispatch.rs delete mode 100644 crates/gpui2/src/focusable.rs create mode 100644 crates/gpui2/src/key_dispatch.rs create mode 100644 crates/gpui2/src/prelude.rs diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index ad17f96055f560210790f783894b18774eec0fab..d432ae037cbbd593ebde48cb5ce08025df422193 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -42,7 +42,7 @@ use gpui::{ action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, - InputHandler, KeyBindingContext, Model, MouseButton, ParentElement, Pixels, Render, + 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, @@ -1980,8 +1980,8 @@ impl Editor { this } - fn dispatch_context(&self, cx: &AppContext) -> KeyBindingContext { - let mut dispatch_context = KeyBindingContext::default(); + 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", diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 2cd319f66bc4d616437b42ba2f7bf72396a6ca83..d42a14bb77dd696f581116192eb384230300e831 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -18,10 +18,9 @@ use gpui::{ black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, - KeyBindingContext, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, - ShapedGlyph, Size, Style, TextRun, TextStyle, TextSystem, ViewContext, WindowContext, - WrappedLineLayout, + KeyContext, KeyDownEvent, KeyListener, 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; @@ -2457,11 +2456,11 @@ 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()), + |_, _| {}, + ) }); } @@ -4165,7 +4164,7 @@ fn build_key_listener( listener: impl Fn( &mut Editor, &T, - &[&KeyBindingContext], + &[&KeyContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui2/src/dispatch.rs b/crates/gpui2/src/dispatch.rs deleted file mode 100644 index 372c8c26104e7cabbffb9ba703af976025faadc3..0000000000000000000000000000000000000000 --- a/crates/gpui2/src/dispatch.rs +++ /dev/null @@ -1,225 +0,0 @@ -use crate::{ - Action, DispatchPhase, FocusId, KeyBindingContext, KeyDownEvent, KeyMatch, Keymap, - KeystrokeMatcher, WindowContext, -}; -use collections::HashMap; -use parking_lot::Mutex; -use smallvec::SmallVec; -use std::{any::Any, sync::Arc}; - -// trait KeyListener -> FnMut(&E, &mut V, &mut ViewContext) -type AnyKeyListener = Box; -type AnyActionListener = Box; - -#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] -pub struct DispatchNodeId(usize); - -pub struct DispatchTree { - node_stack: Vec, - context_stack: Vec, - nodes: Vec, - focused: Option, - focusable_node_ids: HashMap, - keystroke_matchers: HashMap, KeystrokeMatcher>, - keymap: Arc>, -} - -#[derive(Default)] -pub struct DispatchNode { - key_listeners: SmallVec<[AnyKeyListener; 2]>, - action_listeners: SmallVec<[AnyActionListener; 16]>, - context: KeyBindingContext, - parent: Option, -} - -impl DispatchTree { - pub fn clear(&mut self) { - self.node_stack.clear(); - self.nodes.clear(); - } - - pub fn push_node(&mut self, context: Option, old_tree: &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 let Some(context) = context { - self.context_stack.push(context); - if let Some((context_stack, matcher)) = old_tree - .keystroke_matchers - .remove_entry(self.context_stack.as_slice()) - { - self.keystroke_matchers.insert(context_stack, matcher); - } - } - } - - pub fn pop_node(&mut self) -> DispatchNodeId { - self.node_stack.pop().unwrap() - } - - pub fn on_key_event(&mut self, listener: AnyKeyListener) { - self.active_node().key_listeners.push(listener); - } - - pub fn on_action(&mut self, listener: AnyActionListener) { - self.active_node().action_listeners.push(listener); - } - - pub fn make_focusable(&mut self, focus_id: FocusId) { - self.focusable_node_ids - .insert(focus_id, self.active_node_id()); - } - - pub fn set_focus(&mut self, focus_id: Option) { - self.focused = focus_id; - } - - pub fn active_node(&mut self) -> &mut DispatchNode { - let node_id = self.active_node_id(); - &mut self.nodes[node_id.0] - } - - fn active_node_id(&self) -> DispatchNodeId { - *self.node_stack.last().unwrap() - } - - /// Returns the DispatchNodeIds from the root of the tree to the given target node id. - 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 dispatch_key(&mut self, event: &dyn Any, cx: &mut WindowContext) { - if let Some(focused_node_id) = self - .focused - .and_then(|focus_id| self.focusable_node_ids.get(&focus_id)) - .copied() - { - self.dispatch_key_on_node(focused_node_id, event, cx); - } - } - - fn dispatch_key_on_node( - &mut self, - node_id: DispatchNodeId, - event: &dyn Any, - cx: &mut WindowContext, - ) { - let dispatch_path = self.dispatch_path(node_id); - - // Capture phase - self.context_stack.clear(); - cx.propagate_event = true; - for node_id in &dispatch_path { - let node = &self.nodes[node_id.0]; - if !node.context.is_empty() { - self.context_stack.push(node.context.clone()); - } - - for key_listener in &node.key_listeners { - key_listener(event, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } - } - - // Bubble phase - for node_id in dispatch_path.iter().rev() { - let node = &self.nodes[node_id.0]; - - // Handle low level key events - for key_listener in &node.key_listeners { - key_listener(event, DispatchPhase::Bubble, cx); - if !cx.propagate_event { - return; - } - } - - // Match keystrokes - if !node.context.is_empty() { - if let Some(key_down_event) = event.downcast_ref::() { - if !self - .keystroke_matchers - .contains_key(self.context_stack.as_slice()) - { - let keystroke_contexts = self.context_stack.iter().cloned().collect(); - self.keystroke_matchers.insert( - keystroke_contexts, - KeystrokeMatcher::new(self.keymap.clone()), - ); - } - - if let Some(keystroke_matcher) = self - .keystroke_matchers - .get_mut(self.context_stack.as_slice()) - { - if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke( - &key_down_event.keystroke, - self.context_stack.as_slice(), - ) { - self.dispatch_action_on_node(*node_id, action, cx); - if !cx.propagate_event { - return; - } - } - } - } - - self.context_stack.pop(); - } - } - } - - pub fn dispatch_action(&self, action: Box, cx: &mut WindowContext) { - if let Some(focused_node_id) = self - .focused - .and_then(|focus_id| self.focusable_node_ids.get(&focus_id)) - .copied() - { - self.dispatch_action_on_node(focused_node_id, action, cx); - } - } - - fn dispatch_action_on_node( - &self, - node_id: DispatchNodeId, - action: Box, - cx: &mut WindowContext, - ) { - let dispatch_path = self.dispatch_path(node_id); - - // Capture phase - for node_id in &dispatch_path { - let node = &self.nodes[node_id.0]; - for action_listener in &node.action_listeners { - action_listener(&action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } - } - - // Bubble phase - for node_id in dispatch_path.iter().rev() { - let node = &self.nodes[node_id.0]; - for action_listener in &node.action_listeners { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - action_listener(&action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } - } - } -} diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index d88a4119b7232838042ebe53f984b9468e1309c0..eaac9fc71ecc1ac16ab38c9f917c3937c665ac9f 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(), 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(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: handle.clone().into(), 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,17 @@ 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| { 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 +307,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 +340,7 @@ where impl Component for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { fn render(self) -> AnyElement { AnyElement::new(self) @@ -331,7 +350,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 +360,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 +370,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 +379,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 42aea446f1e4bac7c7eaf2399ff72573b9f61242..87de7998a88f9223386b05308871585eeb779040 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -3,17 +3,17 @@ mod action; mod app; mod assets; mod color; -mod dispatch; 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; @@ -42,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 946a59a809b0a758cd74e1ae1bc1c558a53a3704..9ac1f560998eaf64a57e88386c2a13c2dc65c689 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, DispatchPhase, Div, Element, ElementId, FocusHandle, KeyBindingContext, 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,17 +164,6 @@ 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( mut self, @@ -396,25 +385,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| { - 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, @@ -790,7 +760,7 @@ impl ElementInteractivity for StatefulInteractivity { type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; pub struct StatelessInteractivity { - pub dispatch_context: KeyBindingContext, + 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]>, @@ -892,7 +862,7 @@ impl InteractiveElementState { impl Default for StatelessInteractivity { fn default() -> Self { Self { - dispatch_context: KeyBindingContext::default(), + dispatch_context: KeyContext::default(), mouse_down_listeners: SmallVec::new(), mouse_up_listeners: SmallVec::new(), mouse_move_listeners: SmallVec::new(), @@ -1236,7 +1206,7 @@ pub type KeyListener = Box< dyn Fn( &mut V, &dyn Any, - &[&KeyBindingContext], + &[&KeyContext], DispatchPhase, &mut ViewContext, ) -> Option> diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs new file mode 100644 index 0000000000000000000000000000000000000000..9f76df82c3024e2123f4d4141e432865e7c871d5 --- /dev/null +++ b/crates/gpui2/src/key_dispatch.rs @@ -0,0 +1,547 @@ +use crate::{ + build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, + FocusId, KeyContext, KeyDownEvent, KeyMatch, Keymap, 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}, + sync::Arc, +}; +use util::ResultExt; + +type KeyListener = Box; +pub type FocusListeners = SmallVec<[FocusListener; 2]>; +pub type FocusListener = + Box) + 'static>; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] +pub struct DispatchNodeId(usize); + +pub struct KeyDispatcher { + node_stack: Vec, + context_stack: Vec, + nodes: Vec, + focusable_node_ids: HashMap, + keystroke_matchers: HashMap, KeystrokeMatcher>, + keymap: Arc>, +} + +#[derive(Default)] +pub struct DispatchNode { + key_listeners: SmallVec<[KeyListener; 2]>, + action_listeners: SmallVec<[ActionListener; 16]>, + context: KeyContext, + parent: Option, +} + +struct ActionListener { + action_type: TypeId, + listener: Box, +} + +impl KeyDispatcher { + 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(); + } + + 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.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: Box, + ) { + self.active_node().action_listeners.push(ActionListener { + 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 ActionListener { action_type, .. } in &node.action_listeners { + actions.extend(build_action_from_type(action_type).log_err()); + } + } + } + actions + } + + pub fn dispatch_key(&mut self, target: FocusId, event: &dyn Any, cx: &mut WindowContext) { + if let Some(target_node_id) = self.focusable_node_ids.get(&target).copied() { + self.dispatch_key_on_node(target_node_id, event, cx); + } + } + + fn dispatch_key_on_node( + &mut self, + node_id: DispatchNodeId, + event: &dyn Any, + cx: &mut WindowContext, + ) { + let dispatch_path = self.dispatch_path(node_id); + + // Capture phase + self.context_stack.clear(); + cx.propagate_event = true; + for node_id in &dispatch_path { + let node = &self.nodes[node_id.0]; + if !node.context.is_empty() { + self.context_stack.push(node.context.clone()); + } + + for key_listener in &node.key_listeners { + key_listener(event, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = &self.nodes[node_id.0]; + + // Handle low level key events + for key_listener in &node.key_listeners { + key_listener(event, DispatchPhase::Bubble, cx); + if !cx.propagate_event { + return; + } + } + + // Match keystrokes + if !node.context.is_empty() { + if let Some(key_down_event) = event.downcast_ref::() { + if !self + .keystroke_matchers + .contains_key(self.context_stack.as_slice()) + { + let keystroke_contexts = self.context_stack.iter().cloned().collect(); + self.keystroke_matchers.insert( + keystroke_contexts, + KeystrokeMatcher::new(self.keymap.clone()), + ); + } + + if let Some(keystroke_matcher) = self + .keystroke_matchers + .get_mut(self.context_stack.as_slice()) + { + if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke( + &key_down_event.keystroke, + self.context_stack.as_slice(), + ) { + self.dispatch_action_on_node(*node_id, action, cx); + if !cx.propagate_event { + return; + } + } + } + } + + self.context_stack.pop(); + } + } + } + + pub fn dispatch_action( + &self, + target: FocusId, + action: Box, + cx: &mut WindowContext, + ) { + if let Some(target_node_id) = self.focusable_node_ids.get(&target).copied() { + self.dispatch_action_on_node(target_node_id, action, cx); + } + } + + fn dispatch_action_on_node( + &self, + node_id: DispatchNodeId, + action: Box, + cx: &mut WindowContext, + ) { + let dispatch_path = self.dispatch_path(node_id); + + // Capture phase + for node_id in &dispatch_path { + let node = &self.nodes[node_id.0]; + for ActionListener { listener, .. } in &node.action_listeners { + listener(&action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = &self.nodes[node_id.0]; + for ActionListener { listener, .. } in &node.action_listeners { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + listener(&action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } + } + } + } + + fn active_node(&mut self) -> &mut DispatchNode { + let active_node_id = self.active_node_id(); + &mut self.nodes[active_node_id.0] + } + + fn active_node_id(&self) -> DispatchNodeId { + *self.node_stack.last().unwrap() + } + + /// Returns the DispatchNodeIds from the root of the tree to the given target node id. + 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 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 { + 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_key_dispatch(self.key_context().clone(), Some(focus_handle), f) + } 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 FocusableKeyDispatch { + pub key_context: KeyContext, + 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() -> Self { + Self { + key_context: KeyContext::default(), + 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 { + key_context: KeyContext::default(), + 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.key_context + } + + fn key_context_mut(&mut self) -> &mut KeyContext { + &mut self.key_context + } +} + +impl From for FocusableKeyDispatch { + fn from(value: FocusHandle) -> Self { + Self { + key_context: KeyContext::default(), + focus_handle: Some(value), + focus_listeners: FocusListeners::default(), + focus_style: StyleRefinement::default(), + focus_in_style: StyleRefinement::default(), + in_focus_style: StyleRefinement::default(), + } + } +} + +#[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 1cf62484b98c943d33227853b9f2d16f883aae94..9fbd0018b9541edd10ca3fe9dbf67778468f6e66 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -1,4 +1,4 @@ -use crate::{Action, KeyBindingContext, KeyBindingContextPredicate, KeyMatch, Keystroke}; +use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke}; use anyhow::Result; use smallvec::SmallVec; @@ -32,7 +32,7 @@ impl KeyBinding { }) } - pub fn matches_context(&self, contexts: &[KeyBindingContext]) -> bool { + pub fn matches_context(&self, contexts: &[KeyContext]) -> bool { self.context_predicate .as_ref() .map(|predicate| predicate.eval(contexts)) @@ -42,7 +42,7 @@ impl KeyBinding { pub fn match_keystrokes( &self, pending_keystrokes: &[Keystroke], - contexts: &[KeyBindingContext], + contexts: &[KeyContext], ) -> KeyMatch { if self.keystrokes.as_ref().starts_with(&pending_keystrokes) && self.matches_context(contexts) @@ -61,7 +61,7 @@ impl KeyBinding { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[KeyBindingContext], + 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 index 834bd4989a463d01b196a523aacd53869163da1c..b0225e73e79dafd6985102f615b90e059fe7f85e 100644 --- a/crates/gpui2/src/keymap/context.rs +++ b/crates/gpui2/src/keymap/context.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Result}; use smallvec::SmallVec; #[derive(Clone, Debug, Default, Eq, PartialEq, Hash)] -pub struct KeyBindingContext(SmallVec<[ContextEntry; 8]>); +pub struct KeyContext(SmallVec<[ContextEntry; 8]>); #[derive(Clone, Debug, Eq, PartialEq, Hash)] struct ContextEntry { @@ -11,7 +11,7 @@ struct ContextEntry { value: Option, } -impl<'a> TryFrom<&'a str> for KeyBindingContext { +impl<'a> TryFrom<&'a str> for KeyContext { type Error = anyhow::Error; fn try_from(value: &'a str) -> Result { @@ -19,7 +19,7 @@ impl<'a> TryFrom<&'a str> for KeyBindingContext { } } -impl KeyBindingContext { +impl KeyContext { pub fn parse(source: &str) -> Result { let mut context = Self::default(); let source = skip_whitespace(source); @@ -130,7 +130,7 @@ impl KeyBindingContextPredicate { } } - pub fn eval(&self, contexts: &[KeyBindingContext]) -> bool { + pub fn eval(&self, contexts: &[KeyContext]) -> bool { let Some(context) = contexts.last() else { return false; }; @@ -293,19 +293,16 @@ mod tests { #[test] fn test_parse_context() { - let mut expected = KeyBindingContext::default(); + let mut expected = KeyContext::default(); expected.set("foo", "bar"); expected.add("baz"); - assert_eq!(KeyBindingContext::parse("baz foo=bar").unwrap(), expected); - assert_eq!(KeyBindingContext::parse("foo = bar baz").unwrap(), expected); + assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); + assert_eq!(KeyContext::parse("foo = bar baz").unwrap(), expected); assert_eq!( - KeyBindingContext::parse(" baz foo = bar baz").unwrap(), - expected - ); - assert_eq!( - KeyBindingContext::parse(" foo = bar baz").unwrap(), + KeyContext::parse(" baz foo = bar baz").unwrap(), expected ); + assert_eq!(KeyContext::parse(" foo = bar baz").unwrap(), expected); } #[test] diff --git a/crates/gpui2/src/keymap/matcher.rs b/crates/gpui2/src/keymap/matcher.rs index c9b5d26ecbce22d98e001a3b815759fd8605cd9c..bab9c0f5757354b933849ca7b324061df4812cec 100644 --- a/crates/gpui2/src/keymap/matcher.rs +++ b/crates/gpui2/src/keymap/matcher.rs @@ -1,4 +1,4 @@ -use crate::{Action, KeyBindingContext, Keymap, KeymapVersion, Keystroke}; +use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke}; use parking_lot::Mutex; use smallvec::SmallVec; use std::sync::Arc; @@ -44,7 +44,7 @@ impl KeystrokeMatcher { pub fn match_keystroke( &mut self, keystroke: &Keystroke, - context_stack: &[KeyBindingContext], + 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 KeystrokeMatcher { pub fn keystrokes_for_action( &self, action: &dyn Action, - contexts: &[KeyBindingContext], + contexts: &[KeyContext], ) -> Option> { self.keymap .lock() 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 cde7b31754d295739ad91f04dfda38db6ebd9b95..15a59253e7e88d8a1d75ae89360092e213c508c2 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,15 +1,14 @@ use crate::{ - build_action_from_type, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, - AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, - DevicePixels, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, - FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputEvent, IsZero, - KeyBindingContext, KeyListener, KeyMatch, Keystroke, KeystrokeMatcher, 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, + px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, + Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DisplayId, Edges, Effect, + Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, + Hsla, ImageData, InputEvent, IsZero, KeyContext, KeyDispatcher, 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, Result}; use collections::HashMap; @@ -60,16 +59,7 @@ pub enum DispatchPhase { } type AnyObserver = Box bool + 'static>; -type AnyListener = Box; -type AnyKeyListener = Box< - dyn Fn( - &dyn Any, - &[&KeyBindingContext], - DispatchPhase, - &mut WindowContext, - ) -> Option> - + 'static, ->; +type AnyMouseListener = Box; type AnyFocusListener = Box; type AnyWindowFocusListener = Box bool + 'static>; @@ -97,20 +87,12 @@ 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 + .key_dispatcher + .as_ref() + .unwrap() + .focus_contains(*self, other) } } @@ -227,20 +209,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) key_dispatcher: Option, 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(key_dispatcher: KeyDispatcher) -> Self { + Frame { + element_states: HashMap::default(), + mouse_listeners: HashMap::default(), + key_dispatcher: Some(key_dispatcher), + 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 +302,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(KeyDispatcher::new(cx.keymap.clone())), + current_frame: Frame::new(KeyDispatcher::new(cx.keymap.clone())), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_listeners: SubscriberSet::new(), default_prevented: true, @@ -328,18 +321,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(KeyBindingContext), -} - /// 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,7 +388,9 @@ 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) { + let focus_id = handle.id; + + if self.window.focus == Some(focus_id) { return; } @@ -415,13 +398,10 @@ impl<'a> WindowContext<'a> { 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 +421,13 @@ 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| { + let dispatcher = cx.window.current_frame.key_dispatcher.take().unwrap(); + dispatcher.dispatch_action(focus_handle.id, action, cx); + cx.window.current_frame.key_dispatcher = Some(dispatcher); + }) + } } /// Schedules the given function to be run at the end of the current effect cycle, allowing entities @@ -1079,26 +1061,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 +1072,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.key_dispatcher.as_mut().map(KeyDispatcher::clear); } /// Dispatch a mouse or keyboard event on the window. @@ -1226,99 +1185,16 @@ impl<'a> WindowContext<'a> { .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::<[&KeyBindingContext; 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); - } - } + if let Some(focus_id) = self.window.focus { + let mut dispatcher = self.window.current_frame.key_dispatcher.take().unwrap(); + dispatcher.dispatch_key(focus_id, any_key_event, self); + self.window.current_frame.key_dispatcher = Some(dispatcher); } - - 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; - } - } - } - KeyDispatchStackFrame::Context(_) => { - 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: &[KeyBindingContext], - ) -> KeyMatch { - let key_match = 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(); - } - } - - key_match - } - /// Register the given handler to be invoked whenever the global of the given type /// is updated. pub fn observe_global( @@ -1345,105 +1221,16 @@ 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; - } - } - } - } - } - - 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 available_actions(&self) -> Vec> { + if let Some(focus_id) = self.window.focus { + self.window + .current_frame + .key_dispatcher + .as_ref() + .unwrap() + .available_actions(focus_id) + } else { + Vec::new() } } } @@ -1609,22 +1396,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(|| KeystrokeMatcher::new(keymap)), - ); - } - let result = f(global_id, self); let window: &mut Window = self.borrow_mut(); window.element_id_stack.pop(); @@ -2109,94 +1883,25 @@ impl<'a, V: 'static> ViewContext<'a, V> { })); } - pub fn with_key_listeners( + pub fn with_key_dispatch( &mut self, - key_listeners: impl IntoIterator)>, - f: impl FnOnce(&mut Self) -> R, + context: KeyContext, + focus_handle: Option, + f: impl FnOnce(Option, &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: &[&KeyBindingContext], - 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 mut old_dispatcher = self.window.previous_frame.key_dispatcher.take().unwrap(); + let mut current_dispatcher = self.window.current_frame.key_dispatcher.take().unwrap(); - 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( - &mut self, - context: KeyBindingContext, - f: impl FnOnce(&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)); + current_dispatcher.push_node(context, &mut old_dispatcher); + if let Some(focus_handle) = focus_handle.as_ref() { + current_dispatcher.make_focusable(focus_handle.id); } + let result = f(focus_handle, self); + current_dispatcher.pop_node(); - let result = f(self); - - 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 - .current_frame - .focus_parents_by_child - .insert(focus_handle.id, parent_focus_id); - } - self.window.current_frame.focus_stack.push(focus_handle.id); - - if Some(focus_handle.id) == self.window.focus { - self.window.current_frame.freeze_key_dispatch_stack = true; - } - - let result = f(self); + self.window.previous_frame.key_dispatcher = Some(old_dispatcher); + self.window.current_frame.key_dispatcher = Some(current_dispatcher); - self.window.current_frame.focus_stack.pop(); result } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 9d0019b2dc92a156659aca494987c4160f438e12..62c5308dec845c80ea74599f7666168722358b26 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,8 +1,8 @@ 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, FocusableKeyDispatch, ParentElement, Render, + StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext, WindowContext, }; use std::cmp; use theme::ActiveTheme; @@ -137,7 +137,7 @@ impl Picker { } impl Render for Picker { - type Element = Div, FocusEnabled>; + type Element = Div, FocusableKeyDispatch>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 984ee421db3b357545de400547bd5057365610da..368fb20fbf9c6e83ec5616223baf6a89bdcedb9d 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -1,5 +1,5 @@ use gpui::{ - actions, div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, + actions, div, Div, Focusable, FocusableKeyDispatch, KeyBinding, ParentElement, Render, StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, WindowContext, }; use theme2::ActiveTheme; @@ -21,7 +21,7 @@ impl FocusStory { } 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(); diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index 543781ef526552df35bb42b16dee09f051cfeb94..980b31fe5d9f7e5a069d6185e9070a39d0c37090 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}; +use gpui::{Div, ElementInteractivity, KeyDispatch, Styled}; use crate::UITextSize; @@ -69,6 +69,6 @@ pub trait StyledExt: Styled { impl StyledExt for Div where I: ElementInteractivity, - F: ElementFocus, + F: KeyDispatch, { } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 1522b4ec4e6142ff440a32842c8f0229c5eb350f..e55d59303ddbf40a5129873c64b785ad6064bf1d 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -38,10 +38,10 @@ use futures::{ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, GlobalPixels, KeyBindingContext, Model, ModelContext, ParentElement, Point, - Render, Size, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, - Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, - WindowHandle, WindowOptions, + FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, + StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription, Task, + View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, + WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3743,158 +3743,158 @@ impl Render for Workspace { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let mut context = KeyBindingContext::default(); + let mut context = KeyContext::default(); context.add("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()), - // ), + + 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? + 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("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")), + // Panel::new("terminal-panel", cx) + // .child(Terminal::new()) + // .allowed_sides(PanelAllowedSides::BottomOnly) + // .side(PanelSide::Bottom), // ) - // .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!() From 318cb784b2e7fbaca99504d879a208a18590c97c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 10:17:52 +0100 Subject: [PATCH 03/32] Fix panic when calling `with_key_dispatch` recursively Co-Authored-By: Thorsten --- crates/gpui2/src/window.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 15a59253e7e88d8a1d75ae89360092e213c508c2..3da2664f7938503bf6ba846165fe793b3dff70b5 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1889,18 +1889,18 @@ impl<'a, V: 'static> ViewContext<'a, V> { focus_handle: Option, f: impl FnOnce(Option, &mut Self) -> R, ) -> R { - let mut old_dispatcher = self.window.previous_frame.key_dispatcher.take().unwrap(); - let mut current_dispatcher = self.window.current_frame.key_dispatcher.take().unwrap(); + let window = &mut self.window; + let old_dispatcher = window.previous_frame.key_dispatcher.as_mut().unwrap(); + let current_dispatcher = window.current_frame.key_dispatcher.as_mut().unwrap(); - current_dispatcher.push_node(context, &mut old_dispatcher); + current_dispatcher.push_node(context, old_dispatcher); if let Some(focus_handle) = focus_handle.as_ref() { current_dispatcher.make_focusable(focus_handle.id); } let result = f(focus_handle, self); - current_dispatcher.pop_node(); - self.window.previous_frame.key_dispatcher = Some(old_dispatcher); - self.window.current_frame.key_dispatcher = Some(current_dispatcher); + let current_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); + current_dispatcher.pop_node(); result } From 9c182538638ad82690342ef4f8598e8349a3d1bb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 11:37:57 +0100 Subject: [PATCH 04/32] Register key and action listeners using `Interactive::initialize` Co-Authored-By: Thorsten --- crates/editor2/src/element.rs | 362 ++++++++++++------------- crates/gpui2/src/elements/div.rs | 1 + crates/gpui2/src/interactive.rs | 87 +++--- crates/gpui2/src/key_dispatch.rs | 50 ++-- crates/gpui2/src/window.rs | 62 +++++ crates/storybook2/src/stories/focus.rs | 6 +- 6 files changed, 317 insertions(+), 251 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index d42a14bb77dd696f581116192eb384230300e831..b273c5914aa8242349f46a98db6b242450296c09 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -15,10 +15,10 @@ use crate::{ 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, DispatchPhase, Edges, Element, - ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, - KeyContext, KeyDownEvent, KeyListener, KeyMatch, Line, LineLayout, Modifiers, MouseButton, + black, hsla, point, px, relative, size, transparent_black, Action, ActionListener, AnyElement, + AvailableSpace, 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, }; @@ -2459,7 +2459,166 @@ impl Element for EditorElement { cx.with_key_dispatch( dispatch_context, Some(editor.focus_handle.clone()), - |_, _| {}, + |_, cx| { + handle_action(cx, Editor::move_left); + handle_action(cx, Editor::move_right); + handle_action(cx, Editor::move_down); + handle_action(cx, Editor::move_up); + // on_action(cx, Editor::new_file); todo!() + // on_action(cx, Editor::new_file_in_direction); todo!() + handle_action(cx, Editor::cancel); + handle_action(cx, Editor::newline); + handle_action(cx, Editor::newline_above); + handle_action(cx, Editor::newline_below); + handle_action(cx, Editor::backspace); + handle_action(cx, Editor::delete); + handle_action(cx, Editor::tab); + handle_action(cx, Editor::tab_prev); + handle_action(cx, Editor::indent); + handle_action(cx, Editor::outdent); + handle_action(cx, Editor::delete_line); + handle_action(cx, Editor::join_lines); + handle_action(cx, Editor::sort_lines_case_sensitive); + handle_action(cx, Editor::sort_lines_case_insensitive); + handle_action(cx, Editor::reverse_lines); + handle_action(cx, Editor::shuffle_lines); + handle_action(cx, Editor::convert_to_upper_case); + handle_action(cx, Editor::convert_to_lower_case); + handle_action(cx, Editor::convert_to_title_case); + handle_action(cx, Editor::convert_to_snake_case); + handle_action(cx, Editor::convert_to_kebab_case); + handle_action(cx, Editor::convert_to_upper_camel_case); + handle_action(cx, Editor::convert_to_lower_camel_case); + handle_action(cx, Editor::delete_to_previous_word_start); + handle_action(cx, Editor::delete_to_previous_subword_start); + handle_action(cx, Editor::delete_to_next_word_end); + handle_action(cx, Editor::delete_to_next_subword_end); + handle_action(cx, Editor::delete_to_beginning_of_line); + handle_action(cx, Editor::delete_to_end_of_line); + handle_action(cx, Editor::cut_to_end_of_line); + handle_action(cx, Editor::duplicate_line); + handle_action(cx, Editor::move_line_up); + handle_action(cx, Editor::move_line_down); + handle_action(cx, Editor::transpose); + handle_action(cx, Editor::cut); + handle_action(cx, Editor::copy); + handle_action(cx, Editor::paste); + handle_action(cx, Editor::undo); + handle_action(cx, Editor::redo); + handle_action(cx, Editor::move_page_up); + handle_action(cx, Editor::move_page_down); + handle_action(cx, Editor::next_screen); + handle_action(cx, Editor::scroll_cursor_top); + handle_action(cx, Editor::scroll_cursor_center); + handle_action(cx, Editor::scroll_cursor_bottom); + handle_action(cx, |editor, _: &LineDown, cx| { + editor.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + handle_action(cx, |editor, _: &LineUp, cx| { + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + handle_action(cx, |editor, _: &HalfPageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + handle_action(cx, |editor, _: &HalfPageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + handle_action(cx, |editor, _: &PageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + handle_action(cx, |editor, _: &PageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); + handle_action(cx, Editor::move_to_previous_word_start); + handle_action(cx, Editor::move_to_previous_subword_start); + handle_action(cx, Editor::move_to_next_word_end); + handle_action(cx, Editor::move_to_next_subword_end); + handle_action(cx, Editor::move_to_beginning_of_line); + handle_action(cx, Editor::move_to_end_of_line); + handle_action(cx, Editor::move_to_start_of_paragraph); + handle_action(cx, Editor::move_to_end_of_paragraph); + handle_action(cx, Editor::move_to_beginning); + handle_action(cx, Editor::move_to_end); + handle_action(cx, Editor::select_up); + handle_action(cx, Editor::select_down); + handle_action(cx, Editor::select_left); + handle_action(cx, Editor::select_right); + handle_action(cx, Editor::select_to_previous_word_start); + handle_action(cx, Editor::select_to_previous_subword_start); + handle_action(cx, Editor::select_to_next_word_end); + handle_action(cx, Editor::select_to_next_subword_end); + handle_action(cx, Editor::select_to_beginning_of_line); + handle_action(cx, Editor::select_to_end_of_line); + handle_action(cx, Editor::select_to_start_of_paragraph); + handle_action(cx, Editor::select_to_end_of_paragraph); + handle_action(cx, Editor::select_to_beginning); + handle_action(cx, Editor::select_to_end); + handle_action(cx, Editor::select_all); + handle_action(cx, |editor, action, cx| { + editor.select_all_matches(action, cx).log_err(); + }); + handle_action(cx, Editor::select_line); + handle_action(cx, Editor::split_selection_into_lines); + handle_action(cx, Editor::add_selection_above); + handle_action(cx, Editor::add_selection_below); + handle_action(cx, |editor, action, cx| { + editor.select_next(action, cx).log_err(); + }); + handle_action(cx, |editor, action, cx| { + editor.select_previous(action, cx).log_err(); + }); + handle_action(cx, Editor::toggle_comments); + handle_action(cx, Editor::select_larger_syntax_node); + handle_action(cx, Editor::select_smaller_syntax_node); + handle_action(cx, Editor::move_to_enclosing_bracket); + handle_action(cx, Editor::undo_selection); + handle_action(cx, Editor::redo_selection); + handle_action(cx, Editor::go_to_diagnostic); + handle_action(cx, Editor::go_to_prev_diagnostic); + handle_action(cx, Editor::go_to_hunk); + handle_action(cx, Editor::go_to_prev_hunk); + handle_action(cx, Editor::go_to_definition); + handle_action(cx, Editor::go_to_definition_split); + handle_action(cx, Editor::go_to_type_definition); + handle_action(cx, Editor::go_to_type_definition_split); + handle_action(cx, Editor::fold); + handle_action(cx, Editor::fold_at); + handle_action(cx, Editor::unfold_lines); + handle_action(cx, Editor::unfold_at); + handle_action(cx, Editor::fold_selected_ranges); + handle_action(cx, Editor::show_completions); + handle_action(cx, Editor::toggle_code_actions); + // on_action(cx, Editor::open_excerpts); todo!() + handle_action(cx, Editor::toggle_soft_wrap); + handle_action(cx, Editor::toggle_inlay_hints); + handle_action(cx, Editor::reveal_in_finder); + handle_action(cx, Editor::copy_path); + handle_action(cx, Editor::copy_relative_path); + handle_action(cx, Editor::copy_highlight_json); + handle_action(cx, |editor, action, cx| { + editor + .format(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + handle_action(cx, Editor::restart_language_server); + handle_action(cx, Editor::show_character_palette); + // on_action(cx, Editor::confirm_completion); todo!() + handle_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!() + handle_action(cx, Editor::next_copilot_suggestion); + handle_action(cx, Editor::previous_copilot_suggestion); + handle_action(cx, Editor::copilot_suggest); + handle_action(cx, Editor::context_menu_first); + handle_action(cx, Editor::context_menu_prev); + handle_action(cx, Editor::context_menu_next); + handle_action(cx, Editor::context_menu_last); + }, ) }); } @@ -3995,197 +4154,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), - ] -} - -fn build_key_listener( - listener: impl Fn( - &mut Editor, - &T, - &[&KeyContext], - 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 handle_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/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index eaac9fc71ecc1ac16ab38c9f917c3937c665ac9f..7bfd4b244a00bd6211df39d72b81d16f358c422a 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -234,6 +234,7 @@ where element_state.focus_handle.take(), cx, |focus_handle, cx| { + this.interactivity.initialize(cx); element_state.focus_handle = focus_handle; for child in &mut this.children { child.initialize(view_state, cx); diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 9ac1f560998eaf64a57e88386c2a13c2dc65c689..4a7633f8dc6299af9b60eeabdb307ba1b036186c 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -165,43 +165,40 @@ pub trait StatelessInteractive: Element { } /// 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 @@ -214,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 } @@ -232,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 } @@ -439,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, @@ -765,7 +776,9 @@ pub struct StatelessInteractivity { 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]>, @@ -867,7 +880,9 @@ impl Default for StatelessInteractivity { 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(), @@ -1202,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, - &[&KeyContext], - 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 { diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 9f76df82c3024e2123f4d4141e432865e7c871d5..40d6c66973fdd3acaf7ce5159c506175751ee0fe 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -69,6 +69,7 @@ impl KeyDispatcher { }); 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 @@ -153,6 +154,7 @@ impl KeyDispatcher { // Capture phase self.context_stack.clear(); cx.propagate_event = true; + for node_id in &dispatch_path { let node = &self.nodes[node_id.0]; if !node.context.is_empty() { @@ -193,18 +195,16 @@ impl KeyDispatcher { ); } - if let Some(keystroke_matcher) = self + let keystroke_matcher = self .keystroke_matchers .get_mut(self.context_stack.as_slice()) + .unwrap(); + if let KeyMatch::Some(action) = keystroke_matcher + .match_keystroke(&key_down_event.keystroke, self.context_stack.as_slice()) { - if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke( - &key_down_event.keystroke, - self.context_stack.as_slice(), - ) { - self.dispatch_action_on_node(*node_id, action, cx); - if !cx.propagate_event { - return; - } + self.dispatch_action_on_node(*node_id, action, cx); + if !cx.propagate_event { + return; } } } @@ -236,10 +236,17 @@ impl KeyDispatcher { // Capture phase for node_id in &dispatch_path { let node = &self.nodes[node_id.0]; - for ActionListener { listener, .. } in &node.action_listeners { - listener(&action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; + for ActionListener { + action_type, + listener, + } in &node.action_listeners + { + let any_action = action.as_any(); + if *action_type == any_action.type_id() { + listener(any_action, DispatchPhase::Capture, cx); + if !cx.propagate_event { + return; + } } } } @@ -247,11 +254,18 @@ impl KeyDispatcher { // Bubble phase for node_id in dispatch_path.iter().rev() { let node = &self.nodes[node_id.0]; - for ActionListener { listener, .. } in &node.action_listeners { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - listener(&action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; + for ActionListener { + action_type, + listener, + } in &node.action_listeners + { + let any_action = action.as_any(); + if *action_type == any_action.type_id() { + cx.propagate_event = false; // Actions stop propagation by default during the bubble phase + listener(any_action, DispatchPhase::Bubble, cx); + if !cx.propagate_event { + return; + } } } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 3da2664f7938503bf6ba846165fe793b3dff70b5..82d5982475f743c951f8b241c5b1d1fd870b82b5 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -713,6 +713,42 @@ 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, + ) { + let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); + key_dispatcher.on_key_event(Box::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, + ) { + let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); + key_dispatcher.on_action( + action_type, + Box::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 @@ -1955,6 +1991,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/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 368fb20fbf9c6e83ec5616223baf6a89bdcedb9d..142e71cde43a377602b6f408daf53d3a9b257f62 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -39,10 +39,10 @@ impl Render for FocusStory { .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")) @@ -79,7 +79,7 @@ impl Render for FocusStory { .track_focus(&child_2) .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() From 26d26fadb340aafbf497402d34b0ab79a949a65a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 14:35:49 +0100 Subject: [PATCH 05/32] Fix focus story --- crates/storybook2/src/stories/focus.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 142e71cde43a377602b6f408daf53d3a9b257f62..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, Focusable, FocusableKeyDispatch, 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,7 +20,10 @@ 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(), + }) } } @@ -31,8 +38,6 @@ 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") @@ -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,7 +81,7 @@ 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"); From 827b16bf5c7ec811aaf719b4870fc1ddd91e3891 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 14:42:16 +0100 Subject: [PATCH 06/32] Capture node in dispatch tree even if it's not focusable --- crates/gpui2/src/key_dispatch.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 40d6c66973fdd3acaf7ce5159c506175751ee0fe..6dac90fc5968ee73e810a022a6362a5c5cd4acc1 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -305,7 +305,7 @@ pub trait KeyDispatch: 'static { cx: &mut ViewContext, f: impl FnOnce(Option, &mut ViewContext) -> R, ) -> R { - if let Some(focusable) = self.as_focusable_mut() { + 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())) @@ -316,11 +316,12 @@ pub trait KeyDispatch: 'static { listener(view, &focus_handle, event, cx) }); } - - cx.with_key_dispatch(self.key_context().clone(), Some(focus_handle), f) + Some(focus_handle) } else { - f(None, cx) - } + None + }; + + cx.with_key_dispatch(self.key_context().clone(), focus_handle, f) } fn refine_style(&self, style: &mut Style, cx: &WindowContext) { From d0b5c654aae5ea4cf45660aba9bfae936bab2bba Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 14:48:08 +0100 Subject: [PATCH 07/32] Clear pending keystrokes when finding action --- crates/gpui2/src/key_dispatch.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 6dac90fc5968ee73e810a022a6362a5c5cd4acc1..e44dc51c058e4f333c88a23cf070aaa30f0d193f 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -202,6 +202,11 @@ impl KeyDispatcher { if let KeyMatch::Some(action) = keystroke_matcher .match_keystroke(&key_down_event.keystroke, self.context_stack.as_slice()) { + // Clear all pending keystrokes when an action has been found. + for keystroke_matcher in self.keystroke_matchers.values_mut() { + keystroke_matcher.clear_pending(); + } + self.dispatch_action_on_node(*node_id, action, cx); if !cx.propagate_event { return; From c8fb8e2859eb5b9476c6489a2a0299f269f75a1f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 15:20:43 +0100 Subject: [PATCH 08/32] :lipstick: --- crates/editor2/src/element.rs | 252 +++++++++++++++++----------------- 1 file changed, 126 insertions(+), 126 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index b273c5914aa8242349f46a98db6b242450296c09..1b0f3c473c465f781f1fb1356e2ef5c82168e828 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2460,150 +2460,150 @@ impl Element for EditorElement { dispatch_context, Some(editor.focus_handle.clone()), |_, cx| { - handle_action(cx, Editor::move_left); - handle_action(cx, Editor::move_right); - handle_action(cx, Editor::move_down); - handle_action(cx, Editor::move_up); + 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!() - handle_action(cx, Editor::cancel); - handle_action(cx, Editor::newline); - handle_action(cx, Editor::newline_above); - handle_action(cx, Editor::newline_below); - handle_action(cx, Editor::backspace); - handle_action(cx, Editor::delete); - handle_action(cx, Editor::tab); - handle_action(cx, Editor::tab_prev); - handle_action(cx, Editor::indent); - handle_action(cx, Editor::outdent); - handle_action(cx, Editor::delete_line); - handle_action(cx, Editor::join_lines); - handle_action(cx, Editor::sort_lines_case_sensitive); - handle_action(cx, Editor::sort_lines_case_insensitive); - handle_action(cx, Editor::reverse_lines); - handle_action(cx, Editor::shuffle_lines); - handle_action(cx, Editor::convert_to_upper_case); - handle_action(cx, Editor::convert_to_lower_case); - handle_action(cx, Editor::convert_to_title_case); - handle_action(cx, Editor::convert_to_snake_case); - handle_action(cx, Editor::convert_to_kebab_case); - handle_action(cx, Editor::convert_to_upper_camel_case); - handle_action(cx, Editor::convert_to_lower_camel_case); - handle_action(cx, Editor::delete_to_previous_word_start); - handle_action(cx, Editor::delete_to_previous_subword_start); - handle_action(cx, Editor::delete_to_next_word_end); - handle_action(cx, Editor::delete_to_next_subword_end); - handle_action(cx, Editor::delete_to_beginning_of_line); - handle_action(cx, Editor::delete_to_end_of_line); - handle_action(cx, Editor::cut_to_end_of_line); - handle_action(cx, Editor::duplicate_line); - handle_action(cx, Editor::move_line_up); - handle_action(cx, Editor::move_line_down); - handle_action(cx, Editor::transpose); - handle_action(cx, Editor::cut); - handle_action(cx, Editor::copy); - handle_action(cx, Editor::paste); - handle_action(cx, Editor::undo); - handle_action(cx, Editor::redo); - handle_action(cx, Editor::move_page_up); - handle_action(cx, Editor::move_page_down); - handle_action(cx, Editor::next_screen); - handle_action(cx, Editor::scroll_cursor_top); - handle_action(cx, Editor::scroll_cursor_center); - handle_action(cx, Editor::scroll_cursor_bottom); - handle_action(cx, |editor, _: &LineDown, cx| { + 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) }); - handle_action(cx, |editor, _: &LineUp, cx| { + register_action(cx, |editor, _: &LineUp, cx| { editor.scroll_screen(&ScrollAmount::Line(-1.), cx) }); - handle_action(cx, |editor, _: &HalfPageDown, cx| { + register_action(cx, |editor, _: &HalfPageDown, cx| { editor.scroll_screen(&ScrollAmount::Page(0.5), cx) }); - handle_action(cx, |editor, _: &HalfPageUp, cx| { + register_action(cx, |editor, _: &HalfPageUp, cx| { editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) }); - handle_action(cx, |editor, _: &PageDown, cx| { + register_action(cx, |editor, _: &PageDown, cx| { editor.scroll_screen(&ScrollAmount::Page(1.), cx) }); - handle_action(cx, |editor, _: &PageUp, cx| { + register_action(cx, |editor, _: &PageUp, cx| { editor.scroll_screen(&ScrollAmount::Page(-1.), cx) }); - handle_action(cx, Editor::move_to_previous_word_start); - handle_action(cx, Editor::move_to_previous_subword_start); - handle_action(cx, Editor::move_to_next_word_end); - handle_action(cx, Editor::move_to_next_subword_end); - handle_action(cx, Editor::move_to_beginning_of_line); - handle_action(cx, Editor::move_to_end_of_line); - handle_action(cx, Editor::move_to_start_of_paragraph); - handle_action(cx, Editor::move_to_end_of_paragraph); - handle_action(cx, Editor::move_to_beginning); - handle_action(cx, Editor::move_to_end); - handle_action(cx, Editor::select_up); - handle_action(cx, Editor::select_down); - handle_action(cx, Editor::select_left); - handle_action(cx, Editor::select_right); - handle_action(cx, Editor::select_to_previous_word_start); - handle_action(cx, Editor::select_to_previous_subword_start); - handle_action(cx, Editor::select_to_next_word_end); - handle_action(cx, Editor::select_to_next_subword_end); - handle_action(cx, Editor::select_to_beginning_of_line); - handle_action(cx, Editor::select_to_end_of_line); - handle_action(cx, Editor::select_to_start_of_paragraph); - handle_action(cx, Editor::select_to_end_of_paragraph); - handle_action(cx, Editor::select_to_beginning); - handle_action(cx, Editor::select_to_end); - handle_action(cx, Editor::select_all); - handle_action(cx, |editor, action, 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(); }); - handle_action(cx, Editor::select_line); - handle_action(cx, Editor::split_selection_into_lines); - handle_action(cx, Editor::add_selection_above); - handle_action(cx, Editor::add_selection_below); - handle_action(cx, |editor, action, cx| { + 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(); }); - handle_action(cx, |editor, action, cx| { + register_action(cx, |editor, action, cx| { editor.select_previous(action, cx).log_err(); }); - handle_action(cx, Editor::toggle_comments); - handle_action(cx, Editor::select_larger_syntax_node); - handle_action(cx, Editor::select_smaller_syntax_node); - handle_action(cx, Editor::move_to_enclosing_bracket); - handle_action(cx, Editor::undo_selection); - handle_action(cx, Editor::redo_selection); - handle_action(cx, Editor::go_to_diagnostic); - handle_action(cx, Editor::go_to_prev_diagnostic); - handle_action(cx, Editor::go_to_hunk); - handle_action(cx, Editor::go_to_prev_hunk); - handle_action(cx, Editor::go_to_definition); - handle_action(cx, Editor::go_to_definition_split); - handle_action(cx, Editor::go_to_type_definition); - handle_action(cx, Editor::go_to_type_definition_split); - handle_action(cx, Editor::fold); - handle_action(cx, Editor::fold_at); - handle_action(cx, Editor::unfold_lines); - handle_action(cx, Editor::unfold_at); - handle_action(cx, Editor::fold_selected_ranges); - handle_action(cx, Editor::show_completions); - handle_action(cx, Editor::toggle_code_actions); + 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!() - handle_action(cx, Editor::toggle_soft_wrap); - handle_action(cx, Editor::toggle_inlay_hints); - handle_action(cx, Editor::reveal_in_finder); - handle_action(cx, Editor::copy_path); - handle_action(cx, Editor::copy_relative_path); - handle_action(cx, Editor::copy_highlight_json); - handle_action(cx, |editor, action, cx| { + 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)); }); - handle_action(cx, Editor::restart_language_server); - handle_action(cx, Editor::show_character_palette); + register_action(cx, Editor::restart_language_server); + register_action(cx, Editor::show_character_palette); // on_action(cx, Editor::confirm_completion); todo!() - handle_action(cx, |editor, action, cx| { + register_action(cx, |editor, action, cx| { editor .confirm_code_action(action, cx) .map(|task| task.detach_and_log_err(cx)); @@ -2611,13 +2611,13 @@ impl Element for EditorElement { // on_action(cx, Editor::rename); todo!() // on_action(cx, Editor::confirm_rename); todo!() // on_action(cx, Editor::find_all_references); todo!() - handle_action(cx, Editor::next_copilot_suggestion); - handle_action(cx, Editor::previous_copilot_suggestion); - handle_action(cx, Editor::copilot_suggest); - handle_action(cx, Editor::context_menu_first); - handle_action(cx, Editor::context_menu_prev); - handle_action(cx, Editor::context_menu_next); - handle_action(cx, Editor::context_menu_last); + 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); }, ) }); @@ -4154,7 +4154,7 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } -fn handle_action( +fn register_action( cx: &mut ViewContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, ) { From 44534b926d51ff577f6a19b95aab6f90438389ac Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 15:21:47 +0100 Subject: [PATCH 09/32] Register actions on the right div --- crates/gpui2/src/keymap/context.rs | 20 +++++++++++++++++++- crates/workspace2/src/workspace2.rs | 15 ++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/gpui2/src/keymap/context.rs b/crates/gpui2/src/keymap/context.rs index b0225e73e79dafd6985102f615b90e059fe7f85e..99a95531a2601e5cf932c6ba808551d7133b0dea 100644 --- a/crates/gpui2/src/keymap/context.rs +++ b/crates/gpui2/src/keymap/context.rs @@ -1,8 +1,9 @@ use crate::SharedString; use anyhow::{anyhow, Result}; use smallvec::SmallVec; +use std::fmt; -#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)] +#[derive(Clone, Default, Eq, PartialEq, Hash)] pub struct KeyContext(SmallVec<[ContextEntry; 8]>); #[derive(Clone, Debug, Eq, PartialEq, Hash)] @@ -99,6 +100,23 @@ impl KeyContext { } } +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), diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index e55d59303ddbf40a5129873c64b785ad6064bf1d..21c72fd385b2ae54d7a9fa87ab2c95911d27161e 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -39,7 +39,7 @@ use gpui::{ actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size, - StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Subscription, Task, + StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; @@ -534,8 +534,8 @@ pub struct Workspace { workspace_actions: Vec< Box< dyn Fn( - Div>, - ) -> Div>, + Div>, + ) -> Div>, >, >, zoomed: Option, @@ -3514,8 +3514,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,7 +3746,7 @@ impl Render for Workspace { let mut context = KeyContext::default(); context.add("Workspace"); - div() + self.add_workspace_actions_listeners(div()) .context(context) .relative() .size_full() @@ -3761,7 +3761,8 @@ impl Render for Workspace { .child(self.render_titlebar(cx)) .child( // todo! should this be a component a view? - self.add_workspace_actions_listeners(div().id("workspace")) + div() + .id("workspace") .relative() .flex_1() .w_full() From 45fef27aa1807f54f9a0602b0e8d47abffb3ab4c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 15:31:35 +0100 Subject: [PATCH 10/32] Clear all the state when clearing KeyDispatcher --- crates/gpui2/src/key_dispatch.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index e44dc51c058e4f333c88a23cf070aaa30f0d193f..b0f4a5d8d23680bb6b52fc3be22118175c977801 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -58,6 +58,9 @@ impl KeyDispatcher { 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) { From 521972ed9e9bfc7fa8e1cc4e89c2393da0f8d3f3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 11:56:42 -0500 Subject: [PATCH 11/32] Update Status Colors --- crates/theme2/src/colors.rs | 31 +++++++++++++++++++++++++++++ crates/theme2/src/default_colors.rs | 3 +++ 2 files changed, 34 insertions(+) diff --git a/crates/theme2/src/colors.rs b/crates/theme2/src/colors.rs index b8cceebea820f9a7fe61d39bef2d32784006bbc4..aab672ad57c763152b9cf9bd46c08ba1de531899 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/colors.rs @@ -14,16 +14,47 @@ pub struct SystemColors { #[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, } diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 6cfda37a2a632a2d7228152409c1deb1c00c2aa5..3e913905f371a043a5f5ad4c28c24ebebbd3505b 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -122,11 +122,14 @@ impl Default for StatusColors { 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(), } } From 5361a499aedf84b51c591a5d57eeede3c48d9c84 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 12:09:31 -0500 Subject: [PATCH 12/32] Checkpoint --- crates/theme2/src/default_colors.rs | 21 --------------- crates/theme2/src/default_theme.rs | 4 +-- crates/theme2/src/registry.rs | 2 +- crates/theme2/src/styles.rs | 3 +++ crates/theme2/src/styles/status.rs | 41 +++++++++++++++++++++++++++++ crates/theme2/src/theme2.rs | 2 ++ 6 files changed, 49 insertions(+), 24 deletions(-) create mode 100644 crates/theme2/src/styles.rs create mode 100644 crates/theme2/src/styles/status.rs diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 3e913905f371a043a5f5ad4c28c24ebebbd3505b..43d6259e52d28f62acaa3db3e8d5b673a44efa1f 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -114,27 +114,6 @@ impl Default for SystemColors { } } -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(), - 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(), - } - } -} - impl SyntaxTheme { pub fn default_light() -> Self { Self { diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 40fb7df7cfd85d1ed33edcd16ad562af4519ee5d..4e8caf67b13e4c6f01ac5634cb843a155ef0c350 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -13,7 +13,7 @@ fn zed_pro_daylight() -> Theme { styles: ThemeStyles { system: SystemColors::default(), colors: ThemeColors::default_light(), - status: StatusColors::default(), + status: StatusColors::light(), player: PlayerColors::default_light(), syntax: Arc::new(SyntaxTheme::default_light()), }, @@ -28,7 +28,7 @@ pub(crate) fn zed_pro_moonlight() -> Theme { styles: ThemeStyles { system: SystemColors::default(), colors: ThemeColors::default_dark(), - status: StatusColors::default(), + status: StatusColors::dark(), player: PlayerColors::default(), syntax: Arc::new(SyntaxTheme::default_dark()), }, diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 0c61f6f22468901e6c7c78e8664d075c5c82779c..a28c59b6e1ff58e4677f48833fd7a9efd35b7bbf 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -43,7 +43,7 @@ impl ThemeRegistry { }; 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 { diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs new file mode 100644 index 0000000000000000000000000000000000000000..0a44e2a4680cd97389f0e8bcb977d4b47fe3a071 --- /dev/null +++ b/crates/theme2/src/styles.rs @@ -0,0 +1,3 @@ +mod status; + +use status::*; diff --git a/crates/theme2/src/styles/status.rs b/crates/theme2/src/styles/status.rs new file mode 100644 index 0000000000000000000000000000000000000000..87f9da96eee65882fc4b0700105df639f21a7fea --- /dev/null +++ b/crates/theme2/src/styles/status.rs @@ -0,0 +1,41 @@ +use crate::StatusColors; + +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(), + } + } +} diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 7e2085de4e7249ccb1925f1f0c23555f81dd08fc..0d6600eca68914168785843b102f06636d9b507d 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -5,6 +5,7 @@ mod players; mod registry; mod scale; mod settings; +mod styles; mod syntax; #[cfg(not(feature = "importing-themes"))] mod themes; @@ -20,6 +21,7 @@ pub use players::*; pub use registry::*; pub use scale::*; pub use settings::*; +pub use styles::*; pub use syntax::*; #[cfg(not(feature = "importing-themes"))] pub use themes::*; From a6c95ad331899bc0350f19923090982203e97998 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 18:29:18 +0100 Subject: [PATCH 13/32] Fix panic when querying available actions --- crates/editor2/src/element.rs | 12 +- crates/gpui2/src/key_dispatch.rs | 189 +++++---------------- crates/gpui2/src/window.rs | 279 ++++++++++++++++++++++--------- 3 files changed, 249 insertions(+), 231 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 1b0f3c473c465f781f1fb1356e2ef5c82168e828..f8386ee271e658148d7edb9b32fec5c67a044443 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -15,12 +15,12 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - black, hsla, point, px, relative, size, transparent_black, Action, ActionListener, AnyElement, - AvailableSpace, 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, + black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, + 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; diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index b0f4a5d8d23680bb6b52fc3be22118175c977801..bc8e1f8f85b128c8178ba1ea234929c7cf1265bc 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,6 +1,6 @@ use crate::{ build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, - FocusId, KeyContext, KeyDownEvent, KeyMatch, Keymap, KeystrokeMatcher, MouseDownEvent, Pixels, + FocusId, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels, Style, StyleRefinement, ViewContext, WindowContext, }; use collections::HashMap; @@ -9,11 +9,11 @@ use refineable::Refineable; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, + rc::Rc, sync::Arc, }; use util::ResultExt; -type KeyListener = Box; pub type FocusListeners = SmallVec<[FocusListener; 2]>; pub type FocusListener = Box) + 'static>; @@ -21,7 +21,7 @@ pub type FocusListener = #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct DispatchNodeId(usize); -pub struct KeyDispatcher { +pub(crate) struct DispatchTree { node_stack: Vec, context_stack: Vec, nodes: Vec, @@ -31,19 +31,22 @@ pub struct KeyDispatcher { } #[derive(Default)] -pub struct DispatchNode { - key_listeners: SmallVec<[KeyListener; 2]>, - action_listeners: SmallVec<[ActionListener; 16]>, - context: KeyContext, +pub(crate) struct DispatchNode { + pub key_listeners: SmallVec<[KeyListener; 2]>, + pub action_listeners: SmallVec<[ActionListener; 16]>, + pub context: KeyContext, parent: Option, } -struct ActionListener { - action_type: TypeId, - listener: Box, +type KeyListener = Rc; + +#[derive(Clone)] +pub(crate) struct ActionListener { + pub(crate) action_type: TypeId, + pub(crate) listener: Rc, } -impl KeyDispatcher { +impl DispatchTree { pub fn new(keymap: Arc>) -> Self { Self { node_stack: Vec::new(), @@ -97,7 +100,7 @@ impl KeyDispatcher { pub fn on_action( &mut self, action_type: TypeId, - listener: Box, + listener: Rc, ) { self.active_node().action_listeners.push(ActionListener { action_type, @@ -140,143 +143,40 @@ impl KeyDispatcher { actions } - pub fn dispatch_key(&mut self, target: FocusId, event: &dyn Any, cx: &mut WindowContext) { - if let Some(target_node_id) = self.focusable_node_ids.get(&target).copied() { - self.dispatch_key_on_node(target_node_id, event, cx); - } - } - - fn dispatch_key_on_node( + pub fn dispatch_key( &mut self, - node_id: DispatchNodeId, - event: &dyn Any, - cx: &mut WindowContext, - ) { - let dispatch_path = self.dispatch_path(node_id); - - // Capture phase - self.context_stack.clear(); - cx.propagate_event = true; - - for node_id in &dispatch_path { - let node = &self.nodes[node_id.0]; - if !node.context.is_empty() { - self.context_stack.push(node.context.clone()); - } - - for key_listener in &node.key_listeners { - key_listener(event, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } + keystroke: &Keystroke, + context: &[KeyContext], + ) -> Option> { + if !self + .keystroke_matchers + .contains_key(self.context_stack.as_slice()) + { + let keystroke_contexts = self.context_stack.iter().cloned().collect(); + self.keystroke_matchers.insert( + keystroke_contexts, + KeystrokeMatcher::new(self.keymap.clone()), + ); } - // Bubble phase - for node_id in dispatch_path.iter().rev() { - let node = &self.nodes[node_id.0]; - - // Handle low level key events - for key_listener in &node.key_listeners { - key_listener(event, DispatchPhase::Bubble, cx); - if !cx.propagate_event { - return; - } - } - - // Match keystrokes - if !node.context.is_empty() { - if let Some(key_down_event) = event.downcast_ref::() { - if !self - .keystroke_matchers - .contains_key(self.context_stack.as_slice()) - { - let keystroke_contexts = self.context_stack.iter().cloned().collect(); - self.keystroke_matchers.insert( - keystroke_contexts, - KeystrokeMatcher::new(self.keymap.clone()), - ); - } - - let keystroke_matcher = self - .keystroke_matchers - .get_mut(self.context_stack.as_slice()) - .unwrap(); - if let KeyMatch::Some(action) = keystroke_matcher - .match_keystroke(&key_down_event.keystroke, self.context_stack.as_slice()) - { - // Clear all pending keystrokes when an action has been found. - for keystroke_matcher in self.keystroke_matchers.values_mut() { - keystroke_matcher.clear_pending(); - } - - self.dispatch_action_on_node(*node_id, action, cx); - if !cx.propagate_event { - return; - } - } - } - - self.context_stack.pop(); + let keystroke_matcher = self + .keystroke_matchers + .get_mut(self.context_stack.as_slice()) + .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(); } - } - } - pub fn dispatch_action( - &self, - target: FocusId, - action: Box, - cx: &mut WindowContext, - ) { - if let Some(target_node_id) = self.focusable_node_ids.get(&target).copied() { - self.dispatch_action_on_node(target_node_id, action, cx); + Some(action) + } else { + None } } - fn dispatch_action_on_node( - &self, - node_id: DispatchNodeId, - action: Box, - cx: &mut WindowContext, - ) { - let dispatch_path = self.dispatch_path(node_id); - - // Capture phase - for node_id in &dispatch_path { - let node = &self.nodes[node_id.0]; - for ActionListener { - action_type, - listener, - } in &node.action_listeners - { - let any_action = action.as_any(); - if *action_type == any_action.type_id() { - listener(any_action, DispatchPhase::Capture, cx); - if !cx.propagate_event { - return; - } - } - } - } - - // Bubble phase - for node_id in dispatch_path.iter().rev() { - let node = &self.nodes[node_id.0]; - for ActionListener { - action_type, - listener, - } in &node.action_listeners - { - let any_action = action.as_any(); - if *action_type == any_action.type_id() { - cx.propagate_event = false; // Actions stop propagation by default during the bubble phase - listener(any_action, DispatchPhase::Bubble, cx); - if !cx.propagate_event { - return; - } - } - } - } + pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode { + &self.nodes[node_id.0] } fn active_node(&mut self) -> &mut DispatchNode { @@ -288,8 +188,7 @@ impl KeyDispatcher { *self.node_stack.last().unwrap() } - /// Returns the DispatchNodeIds from the root of the tree to the given target node id. - fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> { + 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 { @@ -299,6 +198,10 @@ impl KeyDispatcher { dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node. dispatch_path } + + pub fn focusable_node_id(&self, target: FocusId) -> Option { + self.focusable_node_ids.get(&target).copied() + } } pub trait KeyDispatch: 'static { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 82d5982475f743c951f8b241c5b1d1fd870b82b5..f574d7eb5f5c6ee14031a99bd79aabd59bae1424 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,14 +1,15 @@ use crate::{ - px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, - Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DisplayId, Edges, Effect, - Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, - Hsla, ImageData, InputEvent, IsZero, KeyContext, KeyDispatcher, 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, + key_dispatch::ActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, + AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, + DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, + EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, + InputEvent, IsZero, 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, Result}; use collections::HashMap; @@ -89,9 +90,7 @@ impl FocusId { pub(crate) fn contains(&self, other: Self, cx: &WindowContext) -> bool { cx.window .current_frame - .key_dispatcher - .as_ref() - .unwrap() + .dispatch_tree .focus_contains(*self, other) } } @@ -213,7 +212,7 @@ pub struct Window { pub(crate) struct Frame { element_states: HashMap, mouse_listeners: HashMap>, - pub(crate) key_dispatcher: Option, + pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, pub(crate) scene_builder: SceneBuilder, z_index_stack: StackingOrder, @@ -222,11 +221,11 @@ pub(crate) struct Frame { } impl Frame { - pub fn new(key_dispatcher: KeyDispatcher) -> Self { + pub fn new(dispatch_tree: DispatchTree) -> Self { Frame { element_states: HashMap::default(), mouse_listeners: HashMap::default(), - key_dispatcher: Some(key_dispatcher), + dispatch_tree, focus_listeners: Vec::new(), scene_builder: SceneBuilder::default(), z_index_stack: StackingOrder::default(), @@ -302,8 +301,8 @@ impl Window { layout_engine: TaffyLayoutEngine::new(), root_view: None, element_id_stack: GlobalElementId::default(), - previous_frame: Frame::new(KeyDispatcher::new(cx.keymap.clone())), - current_frame: Frame::new(KeyDispatcher::new(cx.keymap.clone())), + 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, @@ -423,9 +422,14 @@ impl<'a> WindowContext<'a> { pub fn dispatch_action(&mut self, action: Box) { if let Some(focus_handle) = self.focused() { self.defer(move |cx| { - let dispatcher = cx.window.current_frame.key_dispatcher.take().unwrap(); - dispatcher.dispatch_action(focus_handle.id, action, cx); - cx.window.current_frame.key_dispatcher = Some(dispatcher); + 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); + } }) } } @@ -723,12 +727,14 @@ impl<'a> WindowContext<'a> { &mut self, handler: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, ) { - let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); - key_dispatcher.on_key_event(Box::new(move |event, phase, cx| { - if let Some(event) = event.downcast_ref::() { - handler(event, phase, cx) - } - })); + 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 @@ -742,10 +748,9 @@ impl<'a> WindowContext<'a> { action_type: TypeId, handler: impl Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static, ) { - let key_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); - key_dispatcher.on_action( + self.window.current_frame.dispatch_tree.on_action( action_type, - Box::new(move |action, phase, cx| handler(action, phase, cx)), + Rc::new(move |action, phase, cx| handler(action, phase, cx)), ); } @@ -1110,7 +1115,7 @@ impl<'a> WindowContext<'a> { frame.element_states.clear(); frame.mouse_listeners.values_mut().for_each(Vec::clear); frame.focus_listeners.clear(); - frame.key_dispatcher.as_mut().map(KeyDispatcher::clear); + frame.dispatch_tree.clear(); } /// Dispatch a mouse or keyboard event on the window. @@ -1172,63 +1177,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); + !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(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(any_mouse_event, DispatchPhase::Bubble, self); - if !self.app.propagate_event { - break; - } + 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(&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 - && any_mouse_event.downcast_ref::().is_some() - { - self.active_drag = None; + // 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; + } } - // 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(..)), - ); - self.window - .current_frame - .mouse_listeners - .insert(any_mouse_event.type_id(), handlers); + // 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; + } + } + } + + context_stack.pop(); + } } - } else if let Some(any_key_event) = event.keyboard_event() { - if let Some(focus_id) = self.window.focus { - let mut dispatcher = self.window.current_frame.key_dispatcher.take().unwrap(); - dispatcher.dispatch_key(focus_id, any_key_event, self); - self.window.current_frame.key_dispatcher = Some(dispatcher); + } + } + + fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box) { + let dispatch_path = self + .window + .current_frame + .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 ActionListener { + 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; + } + } } } - !self.app.propagate_event + // Bubble phase + for node_id in dispatch_path.iter().rev() { + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for ActionListener { + 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 @@ -1261,9 +1375,7 @@ impl<'a> WindowContext<'a> { if let Some(focus_id) = self.window.focus { self.window .current_frame - .key_dispatcher - .as_ref() - .unwrap() + .dispatch_tree .available_actions(focus_id) } else { Vec::new() @@ -1926,17 +2038,20 @@ impl<'a, V: 'static> ViewContext<'a, V> { f: impl FnOnce(Option, &mut Self) -> R, ) -> R { let window = &mut self.window; - let old_dispatcher = window.previous_frame.key_dispatcher.as_mut().unwrap(); - let current_dispatcher = window.current_frame.key_dispatcher.as_mut().unwrap(); - current_dispatcher.push_node(context, old_dispatcher); + window + .current_frame + .dispatch_tree + .push_node(context, &mut window.previous_frame.dispatch_tree); if let Some(focus_handle) = focus_handle.as_ref() { - current_dispatcher.make_focusable(focus_handle.id); + window + .current_frame + .dispatch_tree + .make_focusable(focus_handle.id); } let result = f(focus_handle, self); - let current_dispatcher = self.window.current_frame.key_dispatcher.as_mut().unwrap(); - current_dispatcher.pop_node(); + self.window.current_frame.dispatch_tree.pop_node(); result } From 348760556aded99003930b03da362e69b9f76347 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 13 Nov 2023 18:33:08 +0100 Subject: [PATCH 14/32] :lipstick: --- crates/gpui2/src/key_dispatch.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index bc8e1f8f85b128c8178ba1ea234929c7cf1265bc..e517b8d34b51eda4473923e09ab8d1e4f0af56a1 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -175,19 +175,6 @@ impl DispatchTree { } } - 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] - } - - fn active_node_id(&self) -> DispatchNodeId { - *self.node_stack.last().unwrap() - } - 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); @@ -199,9 +186,22 @@ impl DispatchTree { 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 { From 7be12cb7b1b9dec84dfed46699eebbce3430ad48 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 12:44:50 -0500 Subject: [PATCH 15/32] Checkpoint Broken --- crates/theme2/src/default_colors.rs | 70 +++++++++++------------ crates/theme2/src/styles.rs | 10 +++- crates/theme2/src/{ => styles}/colors.rs | 9 +-- crates/theme2/src/{ => styles}/players.rs | 0 crates/theme2/src/styles/status.rs | 46 ++++++++++++++- crates/theme2/src/{ => styles}/syntax.rs | 0 crates/theme2/src/styles/system.rs | 9 +++ 7 files changed, 99 insertions(+), 45 deletions(-) rename crates/theme2/src/{ => styles}/colors.rs (98%) rename crates/theme2/src/{ => styles}/players.rs (100%) rename crates/theme2/src/{ => styles}/syntax.rs (100%) create mode 100644 crates/theme2/src/styles/system.rs diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 43d6259e52d28f62acaa3db3e8d5b673a44efa1f..2fd2ec7a8a9acef7bef3029b03cbf01829e9bfee 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -1,6 +1,6 @@ use gpui::{hsla, Hsla, Rgba}; -use crate::colors::{StatusColors, SystemColors, ThemeColors}; +use crate::colors::{SystemColors, ThemeColors}; use crate::scale::{ColorScaleSet, ColorScales}; use crate::syntax::SyntaxTheme; use crate::{ColorScale, PlayerColor, PlayerColors}; @@ -99,7 +99,7 @@ impl PlayerColors { } } -fn neutral() -> ColorScaleSet { +pub(crate) fn neutral() -> ColorScaleSet { slate() } @@ -452,7 +452,7 @@ pub fn default_color_scales() -> ColorScales { } } -fn gray() -> ColorScaleSet { +pub(crate) fn gray() -> ColorScaleSet { StaticColorScaleSet { scale: "Gray", light: [ @@ -516,7 +516,7 @@ fn gray() -> ColorScaleSet { .unwrap() } -fn mauve() -> ColorScaleSet { +pub(crate) fn mauve() -> ColorScaleSet { StaticColorScaleSet { scale: "Mauve", light: [ @@ -580,7 +580,7 @@ fn mauve() -> ColorScaleSet { .unwrap() } -fn slate() -> ColorScaleSet { +pub(crate) fn slate() -> ColorScaleSet { StaticColorScaleSet { scale: "Slate", light: [ @@ -644,7 +644,7 @@ fn slate() -> ColorScaleSet { .unwrap() } -fn sage() -> ColorScaleSet { +pub(crate) fn sage() -> ColorScaleSet { StaticColorScaleSet { scale: "Sage", light: [ @@ -708,7 +708,7 @@ fn sage() -> ColorScaleSet { .unwrap() } -fn olive() -> ColorScaleSet { +pub(crate) fn olive() -> ColorScaleSet { StaticColorScaleSet { scale: "Olive", light: [ @@ -772,7 +772,7 @@ fn olive() -> ColorScaleSet { .unwrap() } -fn sand() -> ColorScaleSet { +pub(crate) fn sand() -> ColorScaleSet { StaticColorScaleSet { scale: "Sand", light: [ @@ -836,7 +836,7 @@ fn sand() -> ColorScaleSet { .unwrap() } -fn gold() -> ColorScaleSet { +pub(crate) fn gold() -> ColorScaleSet { StaticColorScaleSet { scale: "Gold", light: [ @@ -900,7 +900,7 @@ fn gold() -> ColorScaleSet { .unwrap() } -fn bronze() -> ColorScaleSet { +pub(crate) fn bronze() -> ColorScaleSet { StaticColorScaleSet { scale: "Bronze", light: [ @@ -964,7 +964,7 @@ fn bronze() -> ColorScaleSet { .unwrap() } -fn brown() -> ColorScaleSet { +pub(crate) fn brown() -> ColorScaleSet { StaticColorScaleSet { scale: "Brown", light: [ @@ -1028,7 +1028,7 @@ fn brown() -> ColorScaleSet { .unwrap() } -fn yellow() -> ColorScaleSet { +pub(crate) fn yellow() -> ColorScaleSet { StaticColorScaleSet { scale: "Yellow", light: [ @@ -1092,7 +1092,7 @@ fn yellow() -> ColorScaleSet { .unwrap() } -fn amber() -> ColorScaleSet { +pub(crate) fn amber() -> ColorScaleSet { StaticColorScaleSet { scale: "Amber", light: [ @@ -1156,7 +1156,7 @@ fn amber() -> ColorScaleSet { .unwrap() } -fn orange() -> ColorScaleSet { +pub(crate) fn orange() -> ColorScaleSet { StaticColorScaleSet { scale: "Orange", light: [ @@ -1220,7 +1220,7 @@ fn orange() -> ColorScaleSet { .unwrap() } -fn tomato() -> ColorScaleSet { +pub(crate) fn tomato() -> ColorScaleSet { StaticColorScaleSet { scale: "Tomato", light: [ @@ -1284,7 +1284,7 @@ fn tomato() -> ColorScaleSet { .unwrap() } -fn red() -> ColorScaleSet { +pub(crate) fn red() -> ColorScaleSet { StaticColorScaleSet { scale: "Red", light: [ @@ -1348,7 +1348,7 @@ fn red() -> ColorScaleSet { .unwrap() } -fn ruby() -> ColorScaleSet { +pub(crate) fn ruby() -> ColorScaleSet { StaticColorScaleSet { scale: "Ruby", light: [ @@ -1412,7 +1412,7 @@ fn ruby() -> ColorScaleSet { .unwrap() } -fn crimson() -> ColorScaleSet { +pub(crate) fn crimson() -> ColorScaleSet { StaticColorScaleSet { scale: "Crimson", light: [ @@ -1476,7 +1476,7 @@ fn crimson() -> ColorScaleSet { .unwrap() } -fn pink() -> ColorScaleSet { +pub(crate) fn pink() -> ColorScaleSet { StaticColorScaleSet { scale: "Pink", light: [ @@ -1540,7 +1540,7 @@ fn pink() -> ColorScaleSet { .unwrap() } -fn plum() -> ColorScaleSet { +pub(crate) fn plum() -> ColorScaleSet { StaticColorScaleSet { scale: "Plum", light: [ @@ -1604,7 +1604,7 @@ fn plum() -> ColorScaleSet { .unwrap() } -fn purple() -> ColorScaleSet { +pub(crate) fn purple() -> ColorScaleSet { StaticColorScaleSet { scale: "Purple", light: [ @@ -1668,7 +1668,7 @@ fn purple() -> ColorScaleSet { .unwrap() } -fn violet() -> ColorScaleSet { +pub(crate) fn violet() -> ColorScaleSet { StaticColorScaleSet { scale: "Violet", light: [ @@ -1732,7 +1732,7 @@ fn violet() -> ColorScaleSet { .unwrap() } -fn iris() -> ColorScaleSet { +pub(crate) fn iris() -> ColorScaleSet { StaticColorScaleSet { scale: "Iris", light: [ @@ -1796,7 +1796,7 @@ fn iris() -> ColorScaleSet { .unwrap() } -fn indigo() -> ColorScaleSet { +pub(crate) fn indigo() -> ColorScaleSet { StaticColorScaleSet { scale: "Indigo", light: [ @@ -1860,7 +1860,7 @@ fn indigo() -> ColorScaleSet { .unwrap() } -fn blue() -> ColorScaleSet { +pub(crate) fn blue() -> ColorScaleSet { StaticColorScaleSet { scale: "Blue", light: [ @@ -1924,7 +1924,7 @@ fn blue() -> ColorScaleSet { .unwrap() } -fn cyan() -> ColorScaleSet { +pub(crate) fn cyan() -> ColorScaleSet { StaticColorScaleSet { scale: "Cyan", light: [ @@ -1988,7 +1988,7 @@ fn cyan() -> ColorScaleSet { .unwrap() } -fn teal() -> ColorScaleSet { +pub(crate) fn teal() -> ColorScaleSet { StaticColorScaleSet { scale: "Teal", light: [ @@ -2052,7 +2052,7 @@ fn teal() -> ColorScaleSet { .unwrap() } -fn jade() -> ColorScaleSet { +pub(crate) fn jade() -> ColorScaleSet { StaticColorScaleSet { scale: "Jade", light: [ @@ -2116,7 +2116,7 @@ fn jade() -> ColorScaleSet { .unwrap() } -fn green() -> ColorScaleSet { +pub(crate) fn green() -> ColorScaleSet { StaticColorScaleSet { scale: "Green", light: [ @@ -2180,7 +2180,7 @@ fn green() -> ColorScaleSet { .unwrap() } -fn grass() -> ColorScaleSet { +pub(crate) fn grass() -> ColorScaleSet { StaticColorScaleSet { scale: "Grass", light: [ @@ -2244,7 +2244,7 @@ fn grass() -> ColorScaleSet { .unwrap() } -fn lime() -> ColorScaleSet { +pub(crate) fn lime() -> ColorScaleSet { StaticColorScaleSet { scale: "Lime", light: [ @@ -2308,7 +2308,7 @@ fn lime() -> ColorScaleSet { .unwrap() } -fn mint() -> ColorScaleSet { +pub(crate) fn mint() -> ColorScaleSet { StaticColorScaleSet { scale: "Mint", light: [ @@ -2372,7 +2372,7 @@ fn mint() -> ColorScaleSet { .unwrap() } -fn sky() -> ColorScaleSet { +pub(crate) fn sky() -> ColorScaleSet { StaticColorScaleSet { scale: "Sky", light: [ @@ -2436,7 +2436,7 @@ fn sky() -> ColorScaleSet { .unwrap() } -fn black() -> ColorScaleSet { +pub(crate) fn black() -> ColorScaleSet { StaticColorScaleSet { scale: "Black", light: [ @@ -2500,7 +2500,7 @@ fn black() -> ColorScaleSet { .unwrap() } -fn white() -> ColorScaleSet { +pub(crate) fn white() -> ColorScaleSet { StaticColorScaleSet { scale: "White", light: [ diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs index 0a44e2a4680cd97389f0e8bcb977d4b47fe3a071..18f9e76581fb3210da9174378f4fd2e314d19a3b 100644 --- a/crates/theme2/src/styles.rs +++ b/crates/theme2/src/styles.rs @@ -1,3 +1,11 @@ +mod colors; +mod players; mod status; +mod syntax; +mod system; -use status::*; +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 98% rename from crates/theme2/src/colors.rs rename to crates/theme2/src/styles/colors.rs index aab672ad57c763152b9cf9bd46c08ba1de531899..3104f4670561252ecda98bd558eb4425d1f05c85 100644 --- a/crates/theme2/src/colors.rs +++ b/crates/theme2/src/styles/colors.rs @@ -1,15 +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, -} +use crate::{PlayerColors, SyntaxTheme, SystemColors}; #[derive(Refineable, Clone, Debug)] #[refineable(Debug, serde::Deserialize)] diff --git a/crates/theme2/src/players.rs b/crates/theme2/src/styles/players.rs similarity index 100% rename from crates/theme2/src/players.rs rename to crates/theme2/src/styles/players.rs diff --git a/crates/theme2/src/styles/status.rs b/crates/theme2/src/styles/status.rs index 87f9da96eee65882fc4b0700105df639f21a7fea..cf2a1b64f683bb2a00a5928cc1e0611146e9f587 100644 --- a/crates/theme2/src/styles/status.rs +++ b/crates/theme2/src/styles/status.rs @@ -1,4 +1,29 @@ -use crate::StatusColors; +use gpui::Hsla; + +use crate::{blue, grass, neutral, red, yellow, StatusColors}; + +impl Default for StatusColors { + /// Don't use this! + /// We have to have a default for StatusColors to be `[refineable::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 { @@ -38,4 +63,23 @@ impl StatusColors { 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/syntax.rs b/crates/theme2/src/styles/syntax.rs similarity index 100% rename from crates/theme2/src/syntax.rs rename to crates/theme2/src/styles/syntax.rs diff --git a/crates/theme2/src/styles/system.rs b/crates/theme2/src/styles/system.rs new file mode 100644 index 0000000000000000000000000000000000000000..6a7ca101e2603df31b4c5d8a5759ca04d152ff18 --- /dev/null +++ b/crates/theme2/src/styles/system.rs @@ -0,0 +1,9 @@ +use gpui::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, +} From f6c54b804397f2c01625c7ffbdf729042bc23412 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 13:13:40 -0500 Subject: [PATCH 16/32] Redine command palette style Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> Co-Authored-By: Conrad Irwin --- .../command_palette2/src/command_palette.rs | 24 +++++--- crates/picker2/src/picker2.rs | 58 ++++++++++++------- 2 files changed, 52 insertions(+), 30 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index bf9f9fa94b9691405f4ff9f682e6abcf9c6b0b18..508707f2648b6aa4c1145a6b761f3aa31a6f1dd8 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -2,13 +2,13 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, - ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, - WeakView, WindowContext, + ParentElement, Render, SharedString, StatelessInteractive, Styled, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use picker::{Picker, PickerDelegate}; use std::cmp::{self, Reverse}; use theme::ActiveTheme; -use ui::{v_stack, Label, StyledExt}; +use ui::{v_stack, HighlightedLabel, StyledExt}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -147,6 +147,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 +300,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,7 +315,10 @@ 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(HighlightedLabel::new( + command.name.clone(), + r#match.positions.clone(), + )) } // fn render_match( diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index ac1c5f89ea07e058d39ed07173437f3c59cf3c56..1c42e2ed3f13dfe5f5cf81fcb1097fbfbd044e88 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -5,7 +5,7 @@ use gpui::{ WindowContext, }; use std::cmp; -use ui::{prelude::*, v_stack, Divider}; +use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; pub struct Picker { pub delegate: D, @@ -21,7 +21,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 +37,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, @@ -159,23 +163,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)), + ) + }) } } From 8432b713cc53984c2ecf09cae7de038abd2d5443 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 13:16:05 -0500 Subject: [PATCH 17/32] Resolve errors Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/command_palette2/src/command_palette.rs | 9 ++++++--- crates/picker2/src/picker2.rs | 2 +- crates/storybook2/src/stories/picker.rs | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 508707f2648b6aa4c1145a6b761f3aa31a6f1dd8..c7a6c9ee834b81685ca94a0227202a97daaeae8d 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -2,11 +2,14 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, - ParentElement, Render, SharedString, StatelessInteractive, Styled, View, ViewContext, - VisualContext, WeakView, WindowContext, + ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, + 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, HighlightedLabel, StyledExt}; use util::{ diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 1c42e2ed3f13dfe5f5cf81fcb1097fbfbd044e88..0a731b4a27027b59e43f4a8a39f654ad241839c3 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -4,7 +4,7 @@ use gpui::{ StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; -use std::cmp; +use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; pub struct Picker { 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, From 2625051f75adf52c046a1ffd404ef3d2c669c373 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 11:32:05 -0700 Subject: [PATCH 18/32] Better fix for multiple focuses in one frame --- crates/gpui2/src/app.rs | 17 +++++++++++------ crates/gpui2/src/window.rs | 4 ---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 356cf1b76bf5bcf40e15cb7ae9f10e3511db73d1..61c6195d903095105b741caed25e408a49519cf1 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/window.rs b/crates/gpui2/src/window.rs index f574d7eb5f5c6ee14031a99bd79aabd59bae1424..11878c15fa29de5ea38584ad0c739a924d8a25cc 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -389,10 +389,6 @@ impl<'a> WindowContext<'a> { pub fn focus(&mut self, handle: &FocusHandle) { let focus_id = handle.id; - if self.window.focus == Some(focus_id) { - return; - } - if self.window.last_blur.is_none() { self.window.last_blur = Some(self.window.focus); } From 889d20d0464e735557ecce321bb53d975feb8092 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 13:58:23 -0500 Subject: [PATCH 19/32] Reorganize theme2 crate --- crates/theme2/src/default_colors.rs | 238 +--------------------------- crates/theme2/src/default_theme.rs | 16 +- crates/theme2/src/registry.rs | 8 +- crates/theme2/src/styles/colors.rs | 53 +------ crates/theme2/src/styles/players.rs | 103 ++++++++++++ crates/theme2/src/styles/status.rs | 53 ++++++- crates/theme2/src/styles/syntax.rs | 129 +++++++++++++++ crates/theme2/src/styles/system.rs | 13 +- crates/theme2/src/theme2.rs | 6 - 9 files changed, 315 insertions(+), 304 deletions(-) diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 2fd2ec7a8a9acef7bef3029b03cbf01829e9bfee..91efecbfb310103deeafb683becf31b9b7732c42 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -1,243 +1,15 @@ -use gpui::{hsla, Hsla, Rgba}; +use gpui::{Hsla, Rgba}; -use crate::colors::{SystemColors, ThemeColors}; use crate::scale::{ColorScaleSet, ColorScales}; -use crate::syntax::SyntaxTheme; -use crate::{ColorScale, PlayerColor, PlayerColors}; - -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(), - }, - ]) - } -} +use crate::ColorScale; +use crate::{SystemColors, ThemeColors}; 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 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 { @@ -309,7 +81,7 @@ impl ThemeColors { } } - pub fn default_dark() -> Self { + pub fn dark() -> Self { let system = SystemColors::default(); Self { diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 4e8caf67b13e4c6f01ac5634cb843a155ef0c350..95a95c687f5a28dcc78741c89ba4f92c7e5483d4 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::{ - colors::{StatusColors, SystemColors, ThemeColors, ThemeStyles}, - default_color_scales, Appearance, PlayerColors, SyntaxTheme, Theme, ThemeFamily, + default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, + ThemeColors, ThemeFamily, ThemeStyles, }; fn zed_pro_daylight() -> Theme { @@ -12,10 +12,10 @@ fn zed_pro_daylight() -> Theme { appearance: Appearance::Light, styles: ThemeStyles { system: SystemColors::default(), - colors: ThemeColors::default_light(), + colors: ThemeColors::light(), status: StatusColors::light(), - player: PlayerColors::default_light(), - syntax: Arc::new(SyntaxTheme::default_light()), + player: PlayerColors::light(), + syntax: Arc::new(SyntaxTheme::light()), }, } } @@ -27,10 +27,10 @@ pub(crate) fn zed_pro_moonlight() -> Theme { appearance: Appearance::Dark, styles: ThemeStyles { system: SystemColors::default(), - colors: ThemeColors::default_dark(), + colors: ThemeColors::dark(), status: StatusColors::dark(), - player: PlayerColors::default(), - syntax: Arc::new(SyntaxTheme::default_dark()), + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme::dark()), }, } } diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index a28c59b6e1ff58e4677f48833fd7a9efd35b7bbf..c8773ea08bc2a9756bbb2d46784f6a0dfd7a9202 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -38,8 +38,8 @@ 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); @@ -47,8 +47,8 @@ impl ThemeRegistry { 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 diff --git a/crates/theme2/src/styles/colors.rs b/crates/theme2/src/styles/colors.rs index 3104f4670561252ecda98bd558eb4425d1f05c85..1d4917ac00b99797c077623e13f2ac97bde87dd8 100644 --- a/crates/theme2/src/styles/colors.rs +++ b/crates/theme2/src/styles/colors.rs @@ -2,54 +2,7 @@ use gpui::Hsla; use refineable::Refineable; use std::sync::Arc; -use crate::{PlayerColors, SyntaxTheme, SystemColors}; - -#[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, -} +use crate::{PlayerColors, StatusColors, SyntaxTheme, SystemColors}; #[derive(Refineable, Clone, Debug)] #[refineable(Debug, serde::Deserialize)] @@ -283,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); @@ -301,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/styles/players.rs b/crates/theme2/src/styles/players.rs index 0e36ff594732c67ac7f1fd455e849aa259084bc7..68deceb0ff88aa60bebd35ddc202b6edf0aaccc8 100644 --- a/crates/theme2/src/styles/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 index cf2a1b64f683bb2a00a5928cc1e0611146e9f587..db0f4758250b613d2c544f6456bae5c302cba283 100644 --- a/crates/theme2/src/styles/status.rs +++ b/crates/theme2/src/styles/status.rs @@ -1,10 +1,59 @@ use gpui::Hsla; +use refineable::Refineable; -use crate::{blue, grass, neutral, red, yellow, StatusColors}; +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 for StatusColors to be `[refineable::Refinable]`. + /// 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() } diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs index 8aac238555f1190272a8f959da142901fb70d326..8675d30e3a00a94d3ea05efa018dfd7775dabace 100644 --- a/crates/theme2/src/styles/syntax.rs +++ b/crates/theme2/src/styles/syntax.rs @@ -1,13 +1,142 @@ 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 { diff --git a/crates/theme2/src/styles/system.rs b/crates/theme2/src/styles/system.rs index 6a7ca101e2603df31b4c5d8a5759ca04d152ff18..aeb0865155d68aa8e167421b1ce79895120203ff 100644 --- a/crates/theme2/src/styles/system.rs +++ b/crates/theme2/src/styles/system.rs @@ -1,4 +1,4 @@ -use gpui::Hsla; +use gpui::{hsla, Hsla}; #[derive(Clone)] pub struct SystemColors { @@ -7,3 +7,14 @@ pub struct SystemColors { 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/theme2.rs b/crates/theme2/src/theme2.rs index 0d6600eca68914168785843b102f06636d9b507d..06d132d39d0b4c3190fbf11f2cce96076291f3af 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -1,12 +1,9 @@ -mod colors; mod default_colors; mod default_theme; -mod players; mod registry; mod scale; mod settings; mod styles; -mod syntax; #[cfg(not(feature = "importing-themes"))] mod themes; mod user_theme; @@ -14,15 +11,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 styles::*; -pub use syntax::*; #[cfg(not(feature = "importing-themes"))] pub use themes::*; pub use user_theme::*; From 7e7b06553540594feee9e654571f89a079ea37fe Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 12:48:36 -0700 Subject: [PATCH 20/32] Fix on_action on focusable We were accidentally dropping the key context --- crates/go_to_line2/src/go_to_line.rs | 7 ++-- crates/gpui2/src/elements/div.rs | 6 +-- crates/gpui2/src/interactive.rs | 5 ++- crates/gpui2/src/key_dispatch.rs | 55 ++++++++++------------------ crates/gpui2/src/window.rs | 6 +-- crates/picker2/src/picker2.rs | 9 ++--- 6 files changed, 34 insertions(+), 54 deletions(-) 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/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 7bfd4b244a00bd6211df39d72b81d16f358c422a..95c44038ed115ead27a00d2dcd7a10e1fae429dc 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -128,7 +128,7 @@ impl Div, NonFocusableKeyDispatch> { pub fn focusable(self) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity, - key_dispatch: FocusableKeyDispatch::new(), + key_dispatch: FocusableKeyDispatch::new(self.key_dispatch), children: self.children, group: self.group, base_style: self.base_style, @@ -141,7 +141,7 @@ impl Div, NonFocusableKeyDispatch> { ) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity, - key_dispatch: FocusableKeyDispatch::tracked(handle), + key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), children: self.children, group: self.group, base_style: self.base_style, @@ -172,7 +172,7 @@ impl Div, NonFocusableKeyDispatch> { ) -> Div, FocusableKeyDispatch> { Div { interactivity: self.interactivity.into_stateful(handle), - key_dispatch: handle.clone().into(), + key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle), children: self.children, group: self.group, base_style: self.base_style, diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 4a7633f8dc6299af9b60eeabdb307ba1b036186c..aacaeac01ff0dd60dfc096d04a82fb2029704874 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1247,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 index e517b8d34b51eda4473923e09ab8d1e4f0af56a1..8ace4188aed99f0594feddbd6e5b963df7005ebe 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -33,7 +33,7 @@ pub(crate) struct DispatchTree { #[derive(Default)] pub(crate) struct DispatchNode { pub key_listeners: SmallVec<[KeyListener; 2]>, - pub action_listeners: SmallVec<[ActionListener; 16]>, + pub action_listeners: SmallVec<[DispatchActionListener; 16]>, pub context: KeyContext, parent: Option, } @@ -41,7 +41,7 @@ pub(crate) struct DispatchNode { type KeyListener = Rc; #[derive(Clone)] -pub(crate) struct ActionListener { +pub(crate) struct DispatchActionListener { pub(crate) action_type: TypeId, pub(crate) listener: Rc, } @@ -102,10 +102,12 @@ impl DispatchTree { action_type: TypeId, listener: Rc, ) { - self.active_node().action_listeners.push(ActionListener { - action_type, - listener, - }); + self.active_node() + .action_listeners + .push(DispatchActionListener { + action_type, + listener, + }); } pub fn make_focusable(&mut self, focus_id: FocusId) { @@ -135,7 +137,7 @@ impl DispatchTree { 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 ActionListener { action_type, .. } in &node.action_listeners { + for DispatchActionListener { action_type, .. } in &node.action_listeners { actions.extend(build_action_from_type(action_type).log_err()); } } @@ -148,21 +150,15 @@ impl DispatchTree { keystroke: &Keystroke, context: &[KeyContext], ) -> Option> { - if !self - .keystroke_matchers - .contains_key(self.context_stack.as_slice()) - { - let keystroke_contexts = self.context_stack.iter().cloned().collect(); + 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(self.context_stack.as_slice()) - .unwrap(); + 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() { @@ -274,7 +270,7 @@ pub trait KeyDispatch: 'static { } pub struct FocusableKeyDispatch { - pub key_context: KeyContext, + pub non_focusable: NonFocusableKeyDispatch, pub focus_handle: Option, pub focus_listeners: FocusListeners, pub focus_style: StyleRefinement, @@ -283,9 +279,9 @@ pub struct FocusableKeyDispatch { } impl FocusableKeyDispatch { - pub fn new() -> Self { + pub fn new(non_focusable: NonFocusableKeyDispatch) -> Self { Self { - key_context: KeyContext::default(), + non_focusable, focus_handle: None, focus_listeners: FocusListeners::default(), focus_style: StyleRefinement::default(), @@ -294,9 +290,9 @@ impl FocusableKeyDispatch { } } - pub fn tracked(handle: &FocusHandle) -> Self { + pub fn tracked(non_focusable: NonFocusableKeyDispatch, handle: &FocusHandle) -> Self { Self { - key_context: KeyContext::default(), + non_focusable, focus_handle: Some(handle.clone()), focus_listeners: FocusListeners::default(), focus_style: StyleRefinement::default(), @@ -316,24 +312,11 @@ impl KeyDispatch for FocusableKeyDispatch { } fn key_context(&self) -> &KeyContext { - &self.key_context + &self.non_focusable.key_context } fn key_context_mut(&mut self) -> &mut KeyContext { - &mut self.key_context - } -} - -impl From for FocusableKeyDispatch { - fn from(value: FocusHandle) -> Self { - Self { - key_context: KeyContext::default(), - focus_handle: Some(value), - focus_listeners: FocusListeners::default(), - focus_style: StyleRefinement::default(), - focus_in_style: StyleRefinement::default(), - in_focus_style: StyleRefinement::default(), - } + &mut self.non_focusable.key_context } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 11878c15fa29de5ea38584ad0c739a924d8a25cc..4a7241a5c52f0238f50679b5d25005d35cda35c0 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,5 +1,5 @@ use crate::{ - key_dispatch::ActionListener, 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, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, @@ -1306,7 +1306,7 @@ impl<'a> WindowContext<'a> { // Capture phase for node_id in &dispatch_path { let node = self.window.current_frame.dispatch_tree.node(*node_id); - for ActionListener { + for DispatchActionListener { action_type, listener, } in node.action_listeners.clone() @@ -1324,7 +1324,7 @@ impl<'a> WindowContext<'a> { // Bubble phase for node_id in dispatch_path.iter().rev() { let node = self.window.current_frame.dispatch_tree.node(*node_id); - for ActionListener { + for DispatchActionListener { action_type, listener, } in node.action_listeners.clone() diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 62c5308dec845c80ea74599f7666168722358b26..9d75fcb8905e13a5347d4f80136420e881ed2082 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,8 +1,7 @@ use editor::Editor; use gpui::{ - div, uniform_list, Component, Div, FocusableKeyDispatch, 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 theme::ActiveTheme; @@ -137,13 +136,11 @@ impl Picker { } impl Render for Picker { - type Element = Div, FocusableKeyDispatch>; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .context("picker") - .id("picker-container") - .focusable() .size_full() .on_action(Self::select_next) .on_action(Self::select_prev) From 04ad19d01bcddbe7f2cc0f44eeb95b27e4ee1f9a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 13 Nov 2023 15:07:13 -0500 Subject: [PATCH 21/32] Choose appropriate player colors based on theme appearance --- crates/theme2/src/registry.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index c8773ea08bc2a9756bbb2d46784f6a0dfd7a9202..5f576e51f08147024659362f8c0e32d75e2c5e32 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -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), }, } From 916df3c614aeecd976b01bda429f6a52f44e6ff1 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 15:28:20 -0500 Subject: [PATCH 22/32] Add color converter util --- crates/theme2/util/hex_to_hsla.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 crates/theme2/util/hex_to_hsla.py diff --git a/crates/theme2/util/hex_to_hsla.py b/crates/theme2/util/hex_to_hsla.py new file mode 100644 index 0000000000000000000000000000000000000000..f741b9336589f35f9f7b8371579344a58b6ddfd3 --- /dev/null +++ b/crates/theme2/util/hex_to_hsla.py @@ -0,0 +1,20 @@ +import colorsys +import sys + +def hex_to_rgb(hex): + hex = hex.lstrip('#') + return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) + +def rgb_to_hsla(rgb): + h, l, s = colorsys.rgb_to_hls(rgb[0]/255.0, rgb[1]/255.0, rgb[2]/255.0) + return (round(h * 360, 1), round(s * 100, 1), round(l * 100, 1), 1.0) + +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 ") +else: + hex_color = sys.argv[1] + h, s, l, a = hex_to_hsla(hex_color) + print(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") From e0547d9acd2482c8d18445cf977a1665dc9953ec Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 15:47:57 -0500 Subject: [PATCH 23/32] Allow arrays of colors to be passed in --- crates/theme2/util/hex_to_hsla.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/crates/theme2/util/hex_to_hsla.py b/crates/theme2/util/hex_to_hsla.py index f741b9336589f35f9f7b8371579344a58b6ddfd3..17faa186d8c6e6ae78ea907804a1a7b79f78bcfc 100644 --- a/crates/theme2/util/hex_to_hsla.py +++ b/crates/theme2/util/hex_to_hsla.py @@ -3,18 +3,33 @@ import sys def hex_to_rgb(hex): hex = hex.lstrip('#') - return tuple(int(hex[i:i+2], 16) for i in (0, 2, 4)) + 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) - return (round(h * 360, 1), round(s * 100, 1), round(l * 100, 1), 1.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 ") + print("Usage: python util/hex_to_hsla.py <6 or 8 digit hex color or comma-separated list of colors>") else: - hex_color = sys.argv[1] - h, s, l, a = hex_to_hsla(hex_color) - print(f"hsla({h} / 360., {s} / 100., {l} / 100., {a})") + 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})") From c1887747b7b09a76d8b0c43ff804681ab1b57d29 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 15:48:05 -0500 Subject: [PATCH 24/32] Add one_dark theme --- crates/theme2/src/default_theme.rs | 84 +++++++++++----------- crates/theme2/src/one_themes.rs | 107 +++++++++++++++++++++++++++++ crates/theme2/src/registry.rs | 6 +- crates/theme2/src/theme2.rs | 1 + 4 files changed, 154 insertions(+), 44 deletions(-) create mode 100644 crates/theme2/src/one_themes.rs diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 95a95c687f5a28dcc78741c89ba4f92c7e5483d4..0e15c1a9af2399111a21f7801525e634345a73d9 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,58 +1,60 @@ use std::sync::Arc; use crate::{ - default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, - ThemeColors, ThemeFamily, ThemeStyles, + default_color_scales, + one_themes::{one_dark, one_family}, + Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, + ThemeFamily, ThemeStyles, }; -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()), - }, - } -} +// 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::dark(), - status: StatusColors::dark(), - player: PlayerColors::dark(), - syntax: Arc::new(SyntaxTheme::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..e7d6d383ac4031ad90320cc2da03d8ea72989ac9 --- /dev/null +++ b/crates/theme2/src/one_themes.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use gpui::{hsla, rgba}; + +use crate::{ + black, blue, cyan, default_color_scales, green, neutral, red, violet, yellow, 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 = rgba(0x22252A).into(); + // let editor = rgba(0x292C33).into(); + + let bg = hsla(218. / 360., 11. / 100., 15. / 100., 1.); + let editor = hsla(222. / 360., 11. / 100., 18. / 100., 1.); + + 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: bg, + surface_background: bg, + background: bg, + element_background: hsla(222.9 / 360., 11.1 / 100., 24.7 / 100., 1.0), + 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: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + 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: gpui::red(), + 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::red(), + editor_active_wrap_guide: gpui::red(), + editor_document_highlight_read_background: gpui::red(), + editor_document_highlight_write_background: gpui::red(), + terminal_background: bg, + // todo!("Use one colors for terminal") + terminal_ansi_black: black().dark().step_12(), + terminal_ansi_red: red().dark().step_11(), + terminal_ansi_green: green().dark().step_11(), + terminal_ansi_yellow: yellow().dark().step_11(), + terminal_ansi_blue: blue().dark().step_11(), + terminal_ansi_magenta: violet().dark().step_11(), + terminal_ansi_cyan: cyan().dark().step_11(), + terminal_ansi_white: neutral().dark().step_12(), + terminal_ansi_bright_black: black().dark().step_11(), + terminal_ansi_bright_red: red().dark().step_10(), + terminal_ansi_bright_green: green().dark().step_10(), + terminal_ansi_bright_yellow: yellow().dark().step_10(), + terminal_ansi_bright_blue: blue().dark().step_10(), + terminal_ansi_bright_magenta: violet().dark().step_10(), + terminal_ansi_bright_cyan: cyan().dark().step_10(), + terminal_ansi_bright_white: neutral().dark().step_11(), + }, + status: StatusColors::dark(), + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme::dark()), + }, + } +} diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index c8773ea08bc2a9756bbb2d46784f6a0dfd7a9202..ddf1225e5167610ef4cf4e061bfc69e068ca28af 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 { @@ -105,7 +105,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/theme2.rs b/crates/theme2/src/theme2.rs index 06d132d39d0b4c3190fbf11f2cce96076291f3af..b6790b5a6ff534cb8080e7ae88a28c72cbeeefb7 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -1,5 +1,6 @@ mod default_colors; mod default_theme; +mod one_themes; mod registry; mod scale; mod settings; From f8bc9be2841d9f5e54f73de48fe1682ecddf6de9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 13:52:49 -0700 Subject: [PATCH 25/32] Fix test --- crates/gpui2/src/keymap/context.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/gpui2/src/keymap/context.rs b/crates/gpui2/src/keymap/context.rs index 99a95531a2601e5cf932c6ba808551d7133b0dea..b9cb0384ecc6036fa9ac72bd9458987a65ca53a2 100644 --- a/crates/gpui2/src/keymap/context.rs +++ b/crates/gpui2/src/keymap/context.rs @@ -312,15 +312,15 @@ mod tests { #[test] fn test_parse_context() { let mut expected = KeyContext::default(); - expected.set("foo", "bar"); expected.add("baz"); + expected.set("foo", "bar"); assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected); - assert_eq!(KeyContext::parse("foo = bar baz").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(" foo = bar baz").unwrap(), expected); + assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected); } #[test] From 97d6e7f2f533f65884f25a252c8e193ea8174b48 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 17:09:55 -0500 Subject: [PATCH 26/32] Add one syntax theme --- crates/theme2/src/one_themes.rs | 150 +++++++++++++++++++++++++------- 1 file changed, 120 insertions(+), 30 deletions(-) diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index e7d6d383ac4031ad90320cc2da03d8ea72989ac9..b7987391e43342f502dbd9f0e9baf05cb1fc8b42 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use gpui::{hsla, rgba}; +use gpui::{hsla, FontStyle, FontWeight, HighlightStyle}; use crate::{ - black, blue, cyan, default_color_scales, green, neutral, red, violet, yellow, Appearance, - PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeFamily, - ThemeStyles, + default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, + ThemeColors, ThemeFamily, ThemeStyles, }; pub fn one_family() -> ThemeFamily { @@ -19,11 +18,17 @@ pub fn one_family() -> ThemeFamily { } pub(crate) fn one_dark() -> Theme { - // let bg = rgba(0x22252A).into(); - // let editor = rgba(0x292C33).into(); + let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); + let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); - let bg = hsla(218. / 360., 11. / 100., 15. / 100., 1.); - let editor = hsla(222. / 360., 11. / 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(), @@ -61,7 +66,7 @@ pub(crate) fn one_dark() -> Theme { 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: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), + icon_accent: blue.into(), status_bar_background: bg, title_bar_background: bg, toolbar_background: editor, @@ -72,36 +77,121 @@ pub(crate) fn one_dark() -> Theme { 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: gpui::red(), + 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::red(), + editor_wrap_guide: gpui::black(), editor_active_wrap_guide: gpui::red(), - editor_document_highlight_read_background: 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: black().dark().step_12(), - terminal_ansi_red: red().dark().step_11(), - terminal_ansi_green: green().dark().step_11(), - terminal_ansi_yellow: yellow().dark().step_11(), - terminal_ansi_blue: blue().dark().step_11(), - terminal_ansi_magenta: violet().dark().step_11(), - terminal_ansi_cyan: cyan().dark().step_11(), - terminal_ansi_white: neutral().dark().step_12(), - terminal_ansi_bright_black: black().dark().step_11(), - terminal_ansi_bright_red: red().dark().step_10(), - terminal_ansi_bright_green: green().dark().step_10(), - terminal_ansi_bright_yellow: yellow().dark().step_10(), - terminal_ansi_bright_blue: blue().dark().step_10(), - terminal_ansi_bright_magenta: violet().dark().step_10(), - terminal_ansi_bright_cyan: cyan().dark().step_10(), - terminal_ansi_bright_white: neutral().dark().step_11(), + 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, }, - status: StatusColors::dark(), player: PlayerColors::dark(), - syntax: Arc::new(SyntaxTheme::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(), + }), }, } } From dd434588eef4f0cebd5fcfc4179e00c2f435debc Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 17:38:44 -0500 Subject: [PATCH 27/32] WIP --- crates/editor2/src/items.rs | 14 +++++++++----- crates/theme2/src/one_themes.rs | 5 +++-- crates/ui2/src/components/tooltip.rs | 15 +++++++-------- 3 files changed, 19 insertions(+), 15 deletions(-) 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/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index b7987391e43342f502dbd9f0e9baf05cb1fc8b42..6e32eace7350414b002dfda1d3bf242665401e74 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -20,6 +20,7 @@ pub fn one_family() -> ThemeFamily { 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); @@ -43,10 +44,10 @@ pub(crate) fn one_dark() -> Theme { 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: bg, + elevated_surface_background: elevated_surface, surface_background: bg, background: bg, - element_background: hsla(222.9 / 360., 11.1 / 100., 24.7 / 100., 1.0), + 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), 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()) } } From 25bc8988072bfc1430c954ffd4131782b5896f3a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 13 Nov 2023 15:33:22 -0700 Subject: [PATCH 28/32] Add KeyBindings to CommandPalette --- .../command_palette2/src/command_palette.rs | 70 ++------ crates/gpui2/src/key_dispatch.rs | 13 +- crates/gpui2/src/keymap/binding.rs | 16 +- crates/gpui2/src/window.rs | 21 ++- crates/ui2/src/components/keybinding.rs | 157 +++++------------- crates/ui2/src/components/palette.rs | 54 ++---- crates/ui2/src/static_data.rs | 58 ++----- 7 files changed, 116 insertions(+), 273 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index c7a6c9ee834b81685ca94a0227202a97daaeae8d..e403a50cf9ffb087abe1579ee92b6ae64b779780 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -11,7 +11,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme; -use ui::{v_stack, HighlightedLabel, StyledExt}; +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -318,66 +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(HighlightedLabel::new( - command.name.clone(), - r#match.positions.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/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 8ace4188aed99f0594feddbd6e5b963df7005ebe..323fd7d2ff323c8606142b6182cf829961b15cff 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -1,7 +1,7 @@ use crate::{ build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, - FocusId, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, Pixels, - Style, StyleRefinement, ViewContext, WindowContext, + FocusId, KeyBinding, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent, + Pixels, Style, StyleRefinement, ViewContext, WindowContext, }; use collections::HashMap; use parking_lot::Mutex; @@ -145,6 +145,15 @@ impl DispatchTree { 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, diff --git a/crates/gpui2/src/keymap/binding.rs b/crates/gpui2/src/keymap/binding.rs index 9fbd0018b9541edd10ca3fe9dbf67778468f6e66..e55d664610c2ffb90f402016f8589de2f98c8f1d 100644 --- a/crates/gpui2/src/keymap/binding.rs +++ b/crates/gpui2/src/keymap/binding.rs @@ -3,9 +3,19 @@ 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 { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 4a7241a5c52f0238f50679b5d25005d35cda35c0..9eab17815293c87e9f37c3fab37d917895bcf7af 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -3,13 +3,13 @@ use crate::{ AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, - InputEvent, IsZero, 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, + 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, Result}; use collections::HashMap; @@ -1377,6 +1377,13 @@ impl<'a> WindowContext<'a> { Vec::new() } } + + pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { + self.window + .current_frame + .dispatch_tree + .bindings_for_action(action) + } } impl Context for WindowContext<'_> { 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/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"), From 701f954448ac66084eff4c325038eef0bee8c62d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 17:51:58 -0500 Subject: [PATCH 29/32] Start refining tab --- crates/workspace2/src/pane.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2bba684d12c67c2477299ed5915b80a65e7de2d4..008492f1784acfeceab37415d02104d187ba2fd0 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1401,20 +1401,27 @@ 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") + .when(close_right, |this| this.pl_3().pr_1()) + .when(!close_right, |this| 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) + .when(ix < self.active_item_index, |this| this.border_l()) + .when(ix > self.active_item_index, |this| this.border_r()) + .when(ix == self.active_item_index, |this| { + 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( From fea5436ba9322f2afd2a4dc02ef2b00ad29c4637 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 13 Nov 2023 17:54:22 -0500 Subject: [PATCH 30/32] Remove unused imports --- crates/theme2/src/default_theme.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 0e15c1a9af2399111a21f7801525e634345a73d9..8502f433f4a919d7d661f00e55d0dd353ff46fc5 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,10 +1,6 @@ -use std::sync::Arc; - use crate::{ - default_color_scales, one_themes::{one_dark, one_family}, - Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, - ThemeFamily, ThemeStyles, + Theme, ThemeFamily, }; // fn zed_pro_daylight() -> Theme { From 92f2e8eb34c04882076e92ec08cc8d43b42102a4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 13 Nov 2023 18:22:04 -0500 Subject: [PATCH 31/32] Combine related conditions --- crates/workspace2/src/pane.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 008492f1784acfeceab37415d02104d187ba2fd0..d0613e13abc6f63ae9a323bd873400b751063a40 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1405,15 +1405,20 @@ impl Pane { .items_center() .justify_center() // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize") - .when(close_right, |this| this.pl_3().pr_1()) - .when(!close_right, |this| this.pr_1().pr_3()) + .map(|this| { + if close_right { + this.pl_3().pr_1() + } else { + this.pr_1().pr_3() + } + }) .py_1() .bg(tab_bg) .border_color(cx.theme().colors().border) - .when(ix < self.active_item_index, |this| this.border_l()) - .when(ix > self.active_item_index, |this| this.border_r()) - .when(ix == self.active_item_index, |this| { - this.border_l().border_r() + .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)) From 0430e8fbf27f0ce597002d3a8e9b6d13835552d9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 13 Nov 2023 18:44:17 -0500 Subject: [PATCH 32/32] Use "One Dark" as default theme --- crates/storybook2/src/storybook2.rs | 2 +- crates/theme2/src/settings.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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/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(), };